Add basically support for encrypted app list syncing

This commit is contained in:
Jonas Lochmann
2022-07-25 02:00:00 +02:00
parent acdec990ea
commit 8a5e46811e
104 changed files with 5287 additions and 55 deletions
+44
View File
@@ -0,0 +1,44 @@
/*
* server component for the TimeLimit App
* Copyright (C) 2019 - 2022 Jonas Lochmann
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { AppLogicAction } from './basetypes'
import { assertSafeInteger } from './meta/util'
const actionType = 'FinishKeyRequestAction'
export class FinishKeyRequestAction extends AppLogicAction {
readonly deviceSequenceNumber: number
constructor ({ deviceSequenceNumber }: { deviceSequenceNumber: number }) {
super()
assertSafeInteger({ value: deviceSequenceNumber, field: 'deviceSequenceNumber', actionType })
this.deviceSequenceNumber = deviceSequenceNumber
}
static parse = ({ dsn }: SerializedFinishKeyRequestAction) => (
new FinishKeyRequestAction({
deviceSequenceNumber: dsn,
})
)
}
export interface SerializedFinishKeyRequestAction {
type: 'FINISH_KEY_REQUEST'
dsn: number
}
+6 -1
View File
@@ -1,6 +1,6 @@
/*
* server component for the TimeLimit App
* Copyright (C) 2019 - 2021 Jonas Lochmann
* Copyright (C) 2019 - 2022 Jonas Lochmann
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
@@ -30,6 +30,7 @@ export { CreateCategoryAction } from './createcategory'
export { CreateTimeLimitRuleAction } from './createtimelimitrule'
export { DeleteCategoryAction } from './deletecategory'
export { DeleteTimeLimitRuleAction } from './deletetimelimitrule'
export { FinishKeyRequestAction } from './finishkeyrequest'
export { ForceSyncAction } from './forcesync'
export { IgnoreManipulationAction } from './ignoremanipulation'
export { IncrementCategoryExtraTimeAction } from './incrementcategoryextratime'
@@ -38,6 +39,7 @@ export { RemoveInstalledAppsAction } from './removeinstalledapps'
export { RemoveUserAction } from './removeuser'
export { ResetCategoryNetworkIdsAction } from './resetcategorynetworkids'
export { RenameChildAction } from './renamechild'
export { ReplyToKeyRequestAction } from './replytokeyrequest'
export { SetCategoryExtraTimeAction } from './setcategoryextratime'
export { SetCategoryForUnassignedAppsAction } from './setcategoryforunassignedapps'
export { SetChildPasswordAction } from './setchildpassword'
@@ -66,6 +68,7 @@ export { UpdateCategoryTitleAction } from './updatecategorytitle'
export { UpdateDeviceNameAction } from './updatedevicename'
export { UpdateDeviceStatusAction } from './updatedevicestatus'
export { UpdateEnableActivityLevelBlockingAction } from './updateenableactivitylevelblocking'
export { UpdateInstalledAppsAction } from './updateinstalledapps'
export { UpdateNetworkTimeVerificationAction } from './updatenetworktimeverification'
export { UpdateParentNotificationFlagsAction } from './updateparentnotificationflags'
export { UpdateTimelimitRuleAction } from './updatetimelimitrule'
@@ -76,3 +79,5 @@ export { DeleteChildTaskAction } from './deletechildtaskaction'
export { UpdateChildTaskAction } from './updatechildtaskaction'
export { ReviewChildTaskAction } from './reviewchildtaskaction'
export { UpdateUserLimitLoginPreBlockDuration } from './updateuserlimitloginpreblockduration'
export { UploadDevicePublicKeyAction } from './uploaddevicepublickey'
export { SendKeyRequestAction } from './sendkeyrequest'
+74
View File
@@ -0,0 +1,74 @@
/*
* server component for the TimeLimit App
* Copyright (C) 2019 - 2022 Jonas Lochmann
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { AppLogicAction } from './basetypes'
import { InvalidActionParameterException } from './meta/exception'
import { assertSafeInteger } from './meta/util'
const actionType = 'ReplyToKeyRequestAction'
export class ReplyToKeyRequestAction extends AppLogicAction {
readonly requestServerSequenceNumber: number
readonly tempKey: Buffer
readonly encryptedKey: Buffer
readonly signature: Buffer
constructor ({
requestServerSequenceNumber,
tempKey,
encryptedKey,
signature
}: {
requestServerSequenceNumber: number
tempKey: Buffer
encryptedKey: Buffer
signature: Buffer
}) {
super()
assertSafeInteger({ value: requestServerSequenceNumber, field: 'requestServerSequenceNumber', actionType })
if (tempKey.length !== 32 || encryptedKey.length !== 16 || signature.length !== 64) {
throw new InvalidActionParameterException({
actionType,
staticMessage: 'key/signature has wrong length'
})
}
this.requestServerSequenceNumber = requestServerSequenceNumber
this.tempKey = tempKey
this.encryptedKey = encryptedKey
this.signature = signature
}
static parse = ({ rsn, tempKey, encryptedKey, signature }: SerializedReplyToKeyRequestAction) => (
new ReplyToKeyRequestAction({
requestServerSequenceNumber: rsn,
tempKey: Buffer.from(tempKey, 'base64'),
encryptedKey: Buffer.from(encryptedKey, 'base64'),
signature: Buffer.from(signature, 'base64')
})
)
}
export interface SerializedReplyToKeyRequestAction {
type: 'REPLY_TO_KEY_REQUEST'
rsn: number
tempKey: string
encryptedKey: string
signature: string
}
+110
View File
@@ -0,0 +1,110 @@
/*
* server component for the TimeLimit App
* Copyright (C) 2019 - 2022 Jonas Lochmann
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { AppLogicAction } from './basetypes'
import { InvalidActionParameterException } from './meta/exception'
import { assertIdWithinFamily, assertSafeInteger } from './meta/util'
import { types } from '../database/keyrequest'
const actionType = 'SendKeyRequestAction'
export class SendKeyRequestAction extends AppLogicAction {
readonly deviceSequenceNumber: number
readonly deviceId?: string
readonly categoryId?: string
readonly type: number
readonly tempKey: Buffer
readonly signature: Buffer
constructor ({
deviceSequenceNumber,
deviceId,
categoryId,
type,
tempKey,
signature
}: {
deviceSequenceNumber: number
deviceId?: string
categoryId?: string
type: number
tempKey: Buffer
signature: Buffer
}) {
super()
assertSafeInteger({ value: deviceSequenceNumber, field: 'deviceSequenceNumber', actionType })
assertSafeInteger({ value: type, field: 'deviceSequenceNumber', actionType })
if (tempKey.length != 32 || signature.length != 64) {
throw new InvalidActionParameterException({
actionType,
staticMessage: 'key/signature has wrong length'
})
}
if (deviceId !== undefined) {
assertIdWithinFamily({ value: deviceId, actionType, field: 'deviceId' })
}
if (categoryId !== undefined) {
assertIdWithinFamily({ value: categoryId, actionType, field: 'categoryId' })
}
if (deviceId !== undefined && categoryId !== undefined) {
throw new InvalidActionParameterException({
actionType,
staticMessage: 'can not specify device and category at the same time'
})
}
if (types.all.indexOf(type) === -1) {
throw new InvalidActionParameterException({
actionType,
staticMessage: 'invalid type'
})
}
this.deviceSequenceNumber = deviceSequenceNumber
this.deviceId = deviceId
this.categoryId = categoryId
this.type = type
this.tempKey = tempKey
this.signature = signature
}
static parse = ({ dsn, deviceId, categoryId, dataType, tempKey, signature }: SerializedSendKeyRequestAction) => (
new SendKeyRequestAction({
deviceSequenceNumber: dsn,
deviceId,
categoryId,
type: dataType,
tempKey: Buffer.from(tempKey, 'base64'),
signature: Buffer.from(signature, 'base64')
})
)
}
export interface SerializedSendKeyRequestAction {
type: 'SEND_KEY_REQUEST'
dsn: number
deviceId?: string
categoryId?: string
dataType: number
tempKey: string
signature: string
}
+21 -1
View File
@@ -19,26 +19,36 @@ import { AddInstalledAppsAction, SerializedAddInstalledAppsAction } from '../add
import { AddUsedTimeAction, SerializedAddUsedTimeAction } from '../addusedtime'
import { AddUsedTimeActionVersion2, SerializedAddUsedTimeActionVersion2 } from '../addusedtime2'
import { AppLogicAction } from '../basetypes'
import { FinishKeyRequestAction, SerializedFinishKeyRequestAction } from '../finishkeyrequest'
import { ForceSyncAction, SerializedForceSyncAction } from '../forcesync'
import { ReplyToKeyRequestAction, SerializedReplyToKeyRequestAction } from '../replytokeyrequest'
import { MarkTaskPendingAction, SerializedMarkTaskPendingAction } from '../marktaskpendingaction'
import { UnknownActionTypeException } from '../meta/exception'
import { UpdateInstalledAppsAction, SerializedUpdateInstalledAppsAction } from '../updateinstalledapps'
import { RemoveInstalledAppsAction, SerializedRemoveInstalledAppsAction } from '../removeinstalledapps'
import { SendKeyRequestAction, SerializedSendKeyRequestAction } from '../sendkeyrequest'
import { SerializedSignOutAtDeviceAction, SignOutAtDeviceAction } from '../signoutatdevice'
import { SerialiezdTriedDisablingDeviceAdminAction, TriedDisablingDeviceAdminAction } from '../trieddisablingdeviceadmin'
import { SerializedUpdateAppActivitiesAction, UpdateAppActivitiesAction } from '../updateappactivities'
import { SerializedUpdateDeviceStatusAction, UpdateDeviceStatusAction } from '../updatedevicestatus'
import { SerializedUploadDevicePublicKeyAction, UploadDevicePublicKeyAction } from '../uploaddevicepublickey'
export type SerializedAppLogicAction =
SerializedAddInstalledAppsAction |
SerializedAddUsedTimeAction |
SerializedAddUsedTimeActionVersion2 |
SerializedFinishKeyRequestAction |
SerializedForceSyncAction |
SerializedReplyToKeyRequestAction |
SerializedMarkTaskPendingAction |
SerializedUpdateInstalledAppsAction |
SerializedRemoveInstalledAppsAction |
SerializedSendKeyRequestAction |
SerializedSignOutAtDeviceAction |
SerialiezdTriedDisablingDeviceAdminAction |
SerializedUpdateAppActivitiesAction |
SerializedUpdateDeviceStatusAction
SerializedUpdateDeviceStatusAction |
SerializedUploadDevicePublicKeyAction
export const parseAppLogicAction = (serialized: SerializedAppLogicAction): AppLogicAction => {
if (serialized.type === 'ADD_USED_TIME') {
@@ -47,12 +57,20 @@ export const parseAppLogicAction = (serialized: SerializedAppLogicAction): AppLo
return AddUsedTimeActionVersion2.parse(serialized)
} else if (serialized.type === 'ADD_INSTALLED_APPS') {
return AddInstalledAppsAction.parse(serialized)
} else if (serialized.type === 'FINISH_KEY_REQUEST') {
return FinishKeyRequestAction.parse(serialized)
} else if (serialized.type === 'FORCE_SYNC') {
return ForceSyncAction.instance
} else if (serialized.type === 'REPLY_TO_KEY_REQUEST') {
return ReplyToKeyRequestAction.parse(serialized)
} else if (serialized.type === 'MARK_TASK_PENDING') {
return MarkTaskPendingAction.parse(serialized)
} else if (serialized.type === 'UPDATE_INSTALLED_APPS') {
return UpdateInstalledAppsAction.parse(serialized)
} else if (serialized.type === 'REMOVE_INSTALLED_APPS') {
return RemoveInstalledAppsAction.parse(serialized)
} else if (serialized.type === 'SEND_KEY_REQUEST') {
return SendKeyRequestAction.parse(serialized)
} else if (serialized.type === 'SIGN_OUT_AT_DEVICE') {
return SignOutAtDeviceAction.instance
} else if (serialized.type === 'TRIED_DISABLING_DEVICE_ADMIN') {
@@ -61,6 +79,8 @@ export const parseAppLogicAction = (serialized: SerializedAppLogicAction): AppLo
return UpdateAppActivitiesAction.parse(serialized)
} else if (serialized.type === 'UPDATE_DEVICE_STATUS') {
return UpdateDeviceStatusAction.parse(serialized)
} else if (serialized.type === 'UPLOAD_DEVICE_PUBLIC_KEY') {
return UploadDevicePublicKeyAction.parse(serialized)
} else {
throw new UnknownActionTypeException({ group: 'app logic' })
}
+69
View File
@@ -0,0 +1,69 @@
/*
* server component for the TimeLimit App
* Copyright (C) 2019 - 2022 Jonas Lochmann
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { AppLogicAction } from './basetypes'
import { InvalidActionParameterException } from './meta/exception'
const actionType = 'UpdateInstalledAppsAction'
const SIZE_LIMIT = 1024 * 256
export class UpdateInstalledAppsAction extends AppLogicAction {
readonly base?: Buffer
readonly diff?: Buffer
readonly wipe: boolean
constructor ({ base, diff, wipe }: {
base?: Buffer,
diff?: Buffer,
wipe: boolean
}) {
super()
if (base && base.length > SIZE_LIMIT) {
throw new InvalidActionParameterException({
actionType,
staticMessage: 'base data too big'
})
}
if (diff && diff.length > SIZE_LIMIT) {
throw new InvalidActionParameterException({
actionType,
staticMessage: 'diff data too big'
})
}
this.base = base
this.diff = diff
this.wipe = wipe
}
static parse = ({ b, d, w }: SerializedUpdateInstalledAppsAction) => (
new UpdateInstalledAppsAction({
base: b !== undefined ? Buffer.from(b, 'base64') : undefined,
diff: d !== undefined ? Buffer.from(d, 'base64') : undefined,
wipe: w
})
)
}
export interface SerializedUpdateInstalledAppsAction {
type: 'UPDATE_INSTALLED_APPS'
b?: string
d?: string
w: boolean
}
+49
View File
@@ -0,0 +1,49 @@
/*
* server component for the TimeLimit App
* Copyright (C) 2019 - 2022 Jonas Lochmann
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { AppLogicAction } from './basetypes'
import { InvalidActionParameterException } from './meta/exception'
const actionType = 'UploadDevicePublicKeyAction'
export class UploadDevicePublicKeyAction extends AppLogicAction {
readonly key: Buffer
constructor ({ key }: { key: Buffer }) {
super()
if (key.length !== 32) {
throw new InvalidActionParameterException({
actionType,
staticMessage: 'key has wrong length'
})
}
this.key = key
}
static parse = ({ key }: SerializedUploadDevicePublicKeyAction) => (
new UploadDevicePublicKeyAction({
key: Buffer.from(key, 'base64')
})
)
}
export interface SerializedUploadDevicePublicKeyAction {
type: 'UPLOAD_DEVICE_PUBLIC_KEY'
key: string
}
+4 -3
View File
@@ -1,6 +1,6 @@
/*
* server component for the TimeLimit App
* Copyright (C) 2019 - 2020 Jonas Lochmann
* Copyright (C) 2019 - 2022 Jonas Lochmann
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
@@ -90,7 +90,7 @@ export const createSyncRouter = ({ database, websocket, connectedDevicesManager,
where: {
deviceAuthToken: body.deviceAuthToken
},
attributes: ['familyId', 'lastConnectivity'],
attributes: ['familyId', 'deviceId', 'lastConnectivity'],
transaction
})
@@ -98,7 +98,7 @@ export const createSyncRouter = ({ database, websocket, connectedDevicesManager,
throw new Unauthorized()
}
const { familyId, lastConnectivity } = deviceEntryUnsafe
const { familyId, deviceId, lastConnectivity } = deviceEntryUnsafe
const now = getRoundedTimestampForLastConnectivity()
if (parseInt(lastConnectivity, 10) !== now) {
@@ -115,6 +115,7 @@ export const createSyncRouter = ({ database, websocket, connectedDevicesManager,
return generateServerDataStatus({
database,
familyId,
deviceId,
clientStatus: body.status,
transaction
})
+277
View File
@@ -60,6 +60,18 @@ const definitions = {
},
"clientLevel": {
"type": "number"
},
"devicesDetail": {
"type": "object",
"additionalProperties": {
"$ref": "#/definitions/DeviceDataStatus"
}
},
"kri": {
"type": "number"
},
"kr": {
"type": "number"
}
},
"additionalProperties": false,
@@ -97,6 +109,18 @@ const definitions = {
"usedTime"
]
},
"DeviceDataStatus": {
"type": "object",
"properties": {
"appsB": {
"type": "string"
},
"appsD": {
"type": "string"
}
},
"additionalProperties": false
},
"ParentPassword": {
"type": "object",
"properties": {
@@ -1555,6 +1579,25 @@ const definitions = {
"type"
]
},
"SerializedFinishKeyRequestAction": {
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": [
"FINISH_KEY_REQUEST"
]
},
"dsn": {
"type": "number"
}
},
"additionalProperties": false,
"required": [
"dsn",
"type"
]
},
"SerializedForceSyncAction": {
"type": "object",
"properties": {
@@ -1570,6 +1613,37 @@ const definitions = {
"type"
]
},
"SerializedReplyToKeyRequestAction": {
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": [
"REPLY_TO_KEY_REQUEST"
]
},
"rsn": {
"type": "number"
},
"tempKey": {
"type": "string"
},
"encryptedKey": {
"type": "string"
},
"signature": {
"type": "string"
}
},
"additionalProperties": false,
"required": [
"encryptedKey",
"rsn",
"signature",
"tempKey",
"type"
]
},
"SerializedMarkTaskPendingAction": {
"type": "object",
"properties": {
@@ -1589,6 +1663,31 @@ const definitions = {
"type"
]
},
"SerializedUpdateInstalledAppsAction": {
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": [
"UPDATE_INSTALLED_APPS"
]
},
"b": {
"type": "string"
},
"d": {
"type": "string"
},
"w": {
"type": "boolean"
}
},
"additionalProperties": false,
"required": [
"type",
"w"
]
},
"SerializedRemoveInstalledAppsAction": {
"type": "object",
"properties": {
@@ -1611,6 +1710,43 @@ const definitions = {
"type"
]
},
"SerializedSendKeyRequestAction": {
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": [
"SEND_KEY_REQUEST"
]
},
"dsn": {
"type": "number"
},
"deviceId": {
"type": "string"
},
"categoryId": {
"type": "string"
},
"dataType": {
"type": "number"
},
"tempKey": {
"type": "string"
},
"signature": {
"type": "string"
}
},
"additionalProperties": false,
"required": [
"dataType",
"dsn",
"signature",
"tempKey",
"type"
]
},
"SerializedSignOutAtDeviceAction": {
"type": "object",
"properties": {
@@ -1763,6 +1899,25 @@ const definitions = {
"type"
]
},
"SerializedUploadDevicePublicKeyAction": {
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": [
"UPLOAD_DEVICE_PUBLIC_KEY"
]
},
"key": {
"type": "string"
}
},
"additionalProperties": false,
"required": [
"key",
"type"
]
},
"SerializedChildChangePasswordAction": {
"type": "object",
"properties": {
@@ -1916,6 +2071,9 @@ const definitions = {
},
"mFlags": {
"type": "number"
},
"pk": {
"type": "string"
}
},
"additionalProperties": false,
@@ -1978,6 +2136,40 @@ const definitions = {
],
"type": "string"
},
"ServerExtendedDeviceData": {
"type": "object",
"properties": {
"deviceId": {
"type": "string"
},
"appsBase": {
"$ref": "#/definitions/ServerCryptContainer"
},
"appsDiff": {
"$ref": "#/definitions/ServerCryptContainer"
}
},
"additionalProperties": false,
"required": [
"deviceId"
]
},
"ServerCryptContainer": {
"type": "object",
"properties": {
"version": {
"type": "string"
},
"data": {
"type": "string"
}
},
"additionalProperties": false,
"required": [
"data",
"version"
]
},
"ServerInstalledAppsData": {
"type": "object",
"properties": {
@@ -2443,6 +2635,76 @@ const definitions = {
"timeZone",
"type"
]
},
"ServerKeyRequest": {
"type": "object",
"properties": {
"srvSeq": {
"type": "number"
},
"senId": {
"type": "string"
},
"senSeq": {
"type": "number"
},
"deviceId": {
"type": "string"
},
"categoryId": {
"type": "string"
},
"type": {
"type": "number"
},
"tempKey": {
"type": "string"
},
"signature": {
"type": "string"
}
},
"additionalProperties": false,
"required": [
"senId",
"senSeq",
"signature",
"srvSeq",
"tempKey",
"type"
]
},
"ServerKeyResponse": {
"type": "object",
"properties": {
"srvSeq": {
"type": "number"
},
"sender": {
"type": "string"
},
"rqSeq": {
"type": "number"
},
"tempKey": {
"type": "string"
},
"cryptKey": {
"type": "string"
},
"signature": {
"type": "string"
}
},
"additionalProperties": false,
"required": [
"cryptKey",
"rqSeq",
"sender",
"signature",
"srvSeq",
"tempKey"
]
}
}
@@ -2753,15 +3015,27 @@ export const isSerializedAppLogicAction: (value: unknown) => value is Serialized
{
"$ref": "#/definitions/SerializedAddUsedTimeActionVersion2"
},
{
"$ref": "#/definitions/SerializedFinishKeyRequestAction"
},
{
"$ref": "#/definitions/SerializedForceSyncAction"
},
{
"$ref": "#/definitions/SerializedReplyToKeyRequestAction"
},
{
"$ref": "#/definitions/SerializedMarkTaskPendingAction"
},
{
"$ref": "#/definitions/SerializedUpdateInstalledAppsAction"
},
{
"$ref": "#/definitions/SerializedRemoveInstalledAppsAction"
},
{
"$ref": "#/definitions/SerializedSendKeyRequestAction"
},
{
"$ref": "#/definitions/SerializedSignOutAtDeviceAction"
},
@@ -2773,6 +3047,9 @@ export const isSerializedAppLogicAction: (value: unknown) => value is Serialized
},
{
"$ref": "#/definitions/SerializedUpdateDeviceStatusAction"
},
{
"$ref": "#/definitions/SerializedUploadDevicePublicKeyAction"
}
],
"definitions": definitions,
+28 -2
View File
@@ -111,11 +111,19 @@ export interface DeviceAttributesVersion12 {
manipulationFlags: number
}
export interface DeviceAttributesVersion13 {
publicKey: Buffer | null
}
export interface DeviceAttributesVersion14 {
nextKeyReplySequenceNumber: string
}
export type DeviceAttributes = DeviceAttributesVersion1 & DeviceAttributesVersion2 &
DeviceAttributesVersion3 & DeviceAttributesVersion4 & DeviceAttributesVersion5 &
DeviceAttributesVersion6 & DeviceAttributesVersion7 & DeviceAttributesVersion8 &
DeviceAttributesVersion9 & DeviceAttributesVersion10 & DeviceAttributesVersion11 &
DeviceAttributesVersion12
DeviceAttributesVersion12 & DeviceAttributesVersion13 & DeviceAttributesVersion14
export type DeviceModel = Sequelize.Model<DeviceAttributes> & DeviceAttributes
export type DeviceModelStatic = typeof Sequelize.Model & {
@@ -281,6 +289,22 @@ export const attributesVersion12: SequelizeAttributes<DeviceAttributesVersion12>
}
}
export const attributesVersion13: SequelizeAttributes<DeviceAttributesVersion13> = {
publicKey: {
type: Sequelize.BLOB,
allowNull: true,
defaultValue: null
}
}
export const attributesVersion14: SequelizeAttributes<DeviceAttributesVersion14> = {
nextKeyReplySequenceNumber: {
type: Sequelize.BIGINT,
allowNull: false,
defaultValue: 1
}
}
export const attributes: SequelizeAttributes<DeviceAttributes> = {
...attributesVersion1,
...attributesVersion2,
@@ -293,7 +317,9 @@ export const attributes: SequelizeAttributes<DeviceAttributes> = {
...attributesVersion9,
...attributesVersion10,
...attributesVersion11,
...attributesVersion12
...attributesVersion12,
...attributesVersion13,
...attributesVersion14
}
export const createDeviceModel = (sequelize: Sequelize.Sequelize): DeviceModelStatic => sequelize.define('Device', attributes) as DeviceModelStatic
+66
View File
@@ -0,0 +1,66 @@
/*
* server component for the TimeLimit App
* Copyright (C) 2019 - 2022 Jonas Lochmann
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import * as Sequelize from 'sequelize'
import { familyIdColumn, idWithinFamilyColumn, versionColumn } from './columns'
import { SequelizeAttributes } from './types'
export interface EncryptedAppListAttributes {
familyId: string
deviceId: string
type: number
version: string
data: Buffer
}
export const types = {
base: 1,
diff: 2
}
export type EncryptedAppListModel = Sequelize.Model<EncryptedAppListAttributes> & EncryptedAppListAttributes
export type EncryptedAppListModelStatic = typeof Sequelize.Model & {
new (values?: object, options?: Sequelize.BuildOptions): EncryptedAppListModel;
}
export const attributes: SequelizeAttributes<EncryptedAppListAttributes> = {
familyId: {
...familyIdColumn,
primaryKey: true
},
deviceId: {
...idWithinFamilyColumn,
primaryKey: true
},
type: {
type: Sequelize.INTEGER,
primaryKey: true,
validate: {
min: 1,
max: 2
}
},
version: {
...versionColumn
},
data: {
type: Sequelize.BLOB,
allowNull: false
}
}
export const createEncryptedAppListModel = (sequelize: Sequelize.Sequelize): EncryptedAppListModelStatic => sequelize.define('EncryptedAppList', attributes) as EncryptedAppListModelStatic
+25 -3
View File
@@ -1,6 +1,6 @@
/*
* server component for the TimeLimit App
* Copyright (C) 2019 - 2020 Jonas Lochmann
* Copyright (C) 2019 - 2022 Jonas Lochmann
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
@@ -19,7 +19,7 @@ import * as Sequelize from 'sequelize'
import { booleanColumn, familyIdColumn, optionalLabelColumn, timestampColumn, versionColumn } from './columns'
import { SequelizeAttributes } from './types'
export interface FamilyAttributes {
export interface FamilyAttributesVersion1 {
familyId: string
name: string
createdAt: string
@@ -29,12 +29,18 @@ export interface FamilyAttributes {
hasFullVersion: boolean
}
export interface FamilyAttributesVersion2 {
nextServerKeyRequestSeq: string
}
export type FamilyAttributes = FamilyAttributesVersion1 & FamilyAttributesVersion2
export type FamilyModel = Sequelize.Model<FamilyAttributes> & FamilyAttributes
export type FamilyModelStatic = typeof Sequelize.Model & {
new (values?: object, options?: Sequelize.BuildOptions): FamilyModel;
}
export const attributes: SequelizeAttributes<FamilyAttributes> = {
export const attributesVersion1: SequelizeAttributes<FamilyAttributesVersion1> = {
familyId: {
...familyIdColumn,
primaryKey: true
@@ -47,4 +53,20 @@ export const attributes: SequelizeAttributes<FamilyAttributes> = {
hasFullVersion: { ...booleanColumn }
}
export const attributesVersion2: SequelizeAttributes<FamilyAttributesVersion2> = {
nextServerKeyRequestSeq: {
type: Sequelize.BIGINT,
allowNull: false,
defaultValue: 1,
validate: {
min: 1
}
}
}
export const attributes: SequelizeAttributes<FamilyAttributes> = {
...attributesVersion1,
...attributesVersion2
}
export const createFamilyModel = (sequelize: Sequelize.Sequelize): FamilyModelStatic => sequelize.define('Family', attributes) as FamilyModelStatic
+87
View File
@@ -0,0 +1,87 @@
/*
* server component for the TimeLimit App
* Copyright (C) 2019 - 2022 Jonas Lochmann
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import * as Sequelize from 'sequelize'
import { familyIdColumn, idWithinFamilyColumn } from './columns'
import { SequelizeAttributes } from './types'
export interface KeyRequestAttributes {
familyId: string
serverSequenceNumber: string
senderDeviceId: string
senderSequenceNumber: string
deviceId: string | null
categoryId: string | null
type: number
tempKey: Buffer
signature: Buffer
}
export const types = {
appListBase: 1,
appListDiff: 2,
all: [1, 2]
}
export type KeyRequestModel = Sequelize.Model<KeyRequestAttributes> & KeyRequestAttributes
export type KeyRequestModelStatic = typeof Sequelize.Model & {
new (values?: object, options?: Sequelize.BuildOptions): KeyRequestModel;
}
export const attributes: SequelizeAttributes<KeyRequestAttributes> = {
familyId: {
...familyIdColumn,
primaryKey: true
},
serverSequenceNumber: {
type: Sequelize.BIGINT,
primaryKey: true,
allowNull: false
},
senderDeviceId: {
...idWithinFamilyColumn
},
senderSequenceNumber: {
type: Sequelize.BIGINT,
allowNull: false
},
deviceId: {
...idWithinFamilyColumn,
allowNull: true
},
categoryId: {
...idWithinFamilyColumn,
allowNull: true
},
type: {
type: Sequelize.INTEGER,
allowNull: false,
validate: {
isIn: [types.all]
}
},
tempKey: {
type: Sequelize.BLOB,
allowNull: false
},
signature: {
type: Sequelize.BLOB,
allowNull: false
}
}
export const createKeyRequestModel = (sequelize: Sequelize.Sequelize): KeyRequestModelStatic => sequelize.define('KeyRequest', attributes) as KeyRequestModelStatic
+77
View File
@@ -0,0 +1,77 @@
/*
* server component for the TimeLimit App
* Copyright (C) 2019 - 2022 Jonas Lochmann
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import * as Sequelize from 'sequelize'
import { familyIdColumn, idWithinFamilyColumn } from './columns'
import { SequelizeAttributes } from './types'
export interface KeyResponseAttributes {
familyId: string
receiverDeviceId: string
requestServerSequenceNumber: string // fk to request table with familyId
senderDeviceId: string // pk up to this
replyServerSequenceNumber: string // unique with familyId, receiverDeviceId
requestClientSequenceNumber: string
tempKey: Buffer
encryptedKey: Buffer
signature: Buffer
}
export type KeyResponseModel = Sequelize.Model<KeyResponseAttributes> & KeyResponseAttributes
export type KeyResponseModelStatic = typeof Sequelize.Model & {
new (values?: object, options?: Sequelize.BuildOptions): KeyResponseModel;
}
export const attributes: SequelizeAttributes<KeyResponseAttributes> = {
familyId: {
...familyIdColumn,
primaryKey: true
},
receiverDeviceId: {
...idWithinFamilyColumn
},
requestServerSequenceNumber: {
type: Sequelize.BIGINT,
primaryKey: true,
allowNull: false
},
senderDeviceId: {
...idWithinFamilyColumn
},
replyServerSequenceNumber: {
type: Sequelize.BIGINT,
allowNull: false
},
requestClientSequenceNumber: {
type: Sequelize.BIGINT,
allowNull: false
},
tempKey: {
type: Sequelize.BLOB,
allowNull: false
},
encryptedKey: {
type: Sequelize.BLOB,
allowNull: false
},
signature: {
type: Sequelize.BLOB,
allowNull: false
}
}
export const createKeyResponseModel = (sequelize: Sequelize.Sequelize): KeyResponseModelStatic => sequelize.define('KeyResponse', attributes) as KeyResponseModelStatic
+9
View File
@@ -27,7 +27,10 @@ import { CategoryTimeWarningModelStatic, createCategoryTimeWarningModel } from '
import { ChildTaskModelStatic, createChildTaskModel } from './childtask'
import { ConfigModelStatic, createConfigModel } from './config'
import { createDeviceModel, DeviceModelStatic } from './device'
import { createEncryptedAppListModel, EncryptedAppListModelStatic } from './encryptedapplist'
import { createFamilyModel, FamilyModelStatic } from './family'
import { createKeyRequestModel, KeyRequestModelStatic } from './keyrequest'
import { createKeyResponseModel, KeyResponseModelStatic } from './keyresponse'
import { createMailLoginTokenModel, MailLoginTokenModelStatic } from './maillogintoken'
import { createUmzug } from './migration/umzug'
import { createOldDeviceModel, OldDeviceModelStatic } from './olddevice'
@@ -52,7 +55,10 @@ export interface Database {
childTask: ChildTaskModelStatic
config: ConfigModelStatic
device: DeviceModelStatic
encryptedAppList: EncryptedAppListModelStatic
family: FamilyModelStatic
keyRequest: KeyRequestModelStatic
keyResponse: KeyResponseModelStatic
mailLoginToken: MailLoginTokenModelStatic
oldDevice: OldDeviceModelStatic
purchase: PurchaseModelStatic
@@ -77,7 +83,10 @@ const createDatabase = (sequelize: Sequelize.Sequelize): Database => ({
categoryTimeWarning: createCategoryTimeWarningModel(sequelize),
config: createConfigModel(sequelize),
device: createDeviceModel(sequelize),
encryptedAppList: createEncryptedAppListModel(sequelize),
family: createFamilyModel(sequelize),
keyRequest: createKeyRequestModel(sequelize),
keyResponse: createKeyResponseModel(sequelize),
mailLoginToken: createMailLoginTokenModel(sequelize),
oldDevice: createOldDeviceModel(sequelize),
purchase: createPurchaseModel(sequelize),
@@ -1,6 +1,6 @@
/*
* server component for the TimeLimit App
* Copyright (C) 2019 - 2021 Jonas Lochmann
* Copyright (C) 2019 - 2022 Jonas Lochmann
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
@@ -22,7 +22,7 @@ import { attributesVersion1 as authTokenAttributes } from '../../authtoken'
import { attributesVersion1 as categoryAttributes } from '../../category'
import { attributes as categoryAppAttributes } from '../../categoryapp'
import { attributesVersion1 as deviceAttributes } from '../../device'
import { attributes as familyAttributes } from '../../family'
import { attributesVersion1 as familyAttributes } from '../../family'
import { attributes as purchaseAttributes } from '../../purchase'
import { attributesVersion1 as timelimitruleAttributes } from '../../timelimitrule'
import { attributesVersion1 as usedTimeAttribute } from '../../usedtime'
@@ -0,0 +1,62 @@
/*
* server component for the TimeLimit App
* Copyright (C) 2019 - 2022 Jonas Lochmann
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { QueryInterface, Sequelize, Transaction } from 'sequelize'
export async function up (queryInterface: QueryInterface, sequelize: Sequelize) {
await sequelize.transaction({
type: Transaction.TYPES.EXCLUSIVE
}, async (transaction) => {
const dialect = sequelize.getDialect()
const isMysql = dialect === 'mysql' || dialect === 'mariadb'
const isPosgresql = dialect === 'postgres'
if (isMysql) {
await sequelize.query(
'CREATE TABLE `EncryptedAppLists` ' +
'(`familyId` VARCHAR(10) NOT NULL, `deviceId` VARCHAR(6) NOT NULL,' +
'`type` INTEGER NOT NULL, `version` VARCHAR(4) NOT NULL,' +
'`data` BLOB NOT NULL, ' +
'PRIMARY KEY (`familyId`, `deviceId`, `type`),' +
'FOREIGN KEY (`familyId`, `deviceId`) REFERENCES `Devices` (`familyId`, `deviceId`) ON UPDATE CASCADE ON DELETE CASCADE' +
')',
{ transaction }
)
} else {
await sequelize.query(
'CREATE TABLE "EncryptedAppLists" ' +
'("familyId" VARCHAR(10) NOT NULL, "deviceId" VARCHAR(6) NOT NULL,' +
'"type" INTEGER NOT NULL, "version" VARCHAR(4) NOT NULL,' +
'"data" ' + (isPosgresql ? 'BYTEA' : 'BLOB') + ' NOT NULL, ' +
'PRIMARY KEY ("familyId", "deviceId", "type"),' +
'FOREIGN KEY ("familyId", "deviceId") REFERENCES "Devices" ("familyId", "deviceId") ON UPDATE CASCADE ON DELETE CASCADE' +
')',
{ transaction }
)
}
await queryInterface.addIndex('EncryptedAppLists', ['familyId', 'deviceId', 'type', 'version'], { transaction })
})
}
export async function down (queryInterface: QueryInterface, sequelize: Sequelize) {
await sequelize.transaction({
type: Transaction.TYPES.EXCLUSIVE
}, async (transaction) => {
await queryInterface.dropTable('EncryptedAppLists', { transaction })
})
}
@@ -0,0 +1,39 @@
/*
* server component for the TimeLimit App
* Copyright (C) 2019 - 2022 Jonas Lochmann
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { QueryInterface, Sequelize, Transaction } from 'sequelize'
import { attributesVersion13 as deviceAttributes } from '../../device'
export async function up (queryInterface: QueryInterface, sequelize: Sequelize) {
await sequelize.transaction({
type: Transaction.TYPES.EXCLUSIVE
}, async (transaction) => {
await queryInterface.addColumn('Devices', 'publicKey', {
...deviceAttributes.publicKey
}, {
transaction
})
})
}
export async function down (queryInterface: QueryInterface, sequelize: Sequelize) {
await sequelize.transaction({
type: Transaction.TYPES.EXCLUSIVE
}, async (transaction) => {
await queryInterface.removeColumn('Devices', 'publicKey', { transaction })
})
}
@@ -0,0 +1,39 @@
/*
* server component for the TimeLimit App
* Copyright (C) 2019 - 2022 Jonas Lochmann
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { QueryInterface, Sequelize, Transaction } from 'sequelize'
import { attributesVersion2 as familyAttributes } from '../../family'
export async function up (queryInterface: QueryInterface, sequelize: Sequelize) {
await sequelize.transaction({
type: Transaction.TYPES.EXCLUSIVE
}, async (transaction) => {
await queryInterface.addColumn('Families', 'nextServerKeyRequestSeq', {
...familyAttributes.nextServerKeyRequestSeq
}, {
transaction
})
})
}
export async function down (queryInterface: QueryInterface, sequelize: Sequelize) {
await sequelize.transaction({
type: Transaction.TYPES.EXCLUSIVE
}, async (transaction) => {
await queryInterface.removeColumn('Families', 'nextServerKeyRequestSeq', { transaction })
})
}
@@ -0,0 +1,81 @@
/*
* server component for the TimeLimit App
* Copyright (C) 2019 - 2022 Jonas Lochmann
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { QueryInterface, Sequelize, Transaction } from 'sequelize'
export async function up (queryInterface: QueryInterface, sequelize: Sequelize) {
await sequelize.transaction({
type: Transaction.TYPES.EXCLUSIVE
}, async (transaction) => {
const dialect = sequelize.getDialect()
const isMysql = dialect === 'mysql' || dialect === 'mariadb'
const isPosgresql = dialect === 'postgres'
if (isMysql) {
await sequelize.query(
'CREATE TABLE `KeyRequests` (' +
'`familyId` VARCHAR(10) NOT NULL, ' +
'`serverSequenceNumber` BIGINT NOT NULL, ' +
'`senderDeviceId` VARCHAR(6) NOT NULL, ' +
'`senderSequenceNumber` BIGINT NOT NULL, ' +
'`deviceId` VARCHAR(6) NULL, ' +
'`categoryId` VARCHAR(6) NULL, ' +
'`type` INTEGER NOT NULL, ' +
'`tempKey` BLOB NOT NULL, ' +
'`signature` BLOB NOT NULL, ' +
'PRIMARY KEY (`familyId`, `serverSequenceNumber`), ' +
'FOREIGN KEY (`familyId`, `senderDeviceId`) REFERENCES `Devices` (`familyId`, `deviceId`) ON UPDATE CASCADE ON DELETE CASCADE, ' +
'FOREIGN KEY (`familyId`, `deviceId`) REFERENCES `Devices` (`familyId`, `deviceId`) ON UPDATE CASCADE ON DELETE CASCADE, ' +
'FOREIGN KEY (`familyId`, `categoryId`) REFERENCES `Categories` (`familyId`, `categoryId`) ON UPDATE CASCADE ON DELETE CASCADE' +
')',
{ transaction }
)
} else {
await sequelize.query(
'CREATE TABLE "KeyRequests" (' +
'"familyId" VARCHAR(10) NOT NULL, ' +
'"serverSequenceNumber" ' + (isPosgresql ? 'BIGINT' : 'LONG') + ' NOT NULL, ' +
'"senderDeviceId" VARCHAR(6) NOT NULL, ' +
'"senderSequenceNumber" ' + (isPosgresql ? 'BIGINT' : 'LONG') + ' NOT NULL, ' +
'"deviceId" VARCHAR(6) NULL, ' +
'"categoryId" VARCHAR(6) NULL, ' +
'"type" INTEGER NOT NULL, ' +
'"tempKey" ' + (isPosgresql ? 'BYTEA' : 'BLOB') + ' NOT NULL, ' +
'"signature" ' + (isPosgresql ? 'BYTEA' : 'BLOB') + ' NOT NULL, ' +
'PRIMARY KEY ("familyId", "serverSequenceNumber"), ' +
'FOREIGN KEY ("familyId", "senderDeviceId") REFERENCES "Devices" ("familyId", "deviceId") ON UPDATE CASCADE ON DELETE CASCADE, ' +
'FOREIGN KEY ("familyId", "deviceId") REFERENCES "Devices" ("familyId", "deviceId") ON UPDATE CASCADE ON DELETE CASCADE, ' +
'FOREIGN KEY ("familyId", "categoryId") REFERENCES "Categories" ("familyId", "categoryId") ON UPDATE CASCADE ON DELETE CASCADE' +
')',
{ transaction }
)
}
await queryInterface.addIndex('KeyRequests', ['familyId', 'senderDeviceId', 'senderSequenceNumber'], { transaction })
await queryInterface.addIndex('KeyRequests', ['familyId', 'deviceId'], { transaction })
await queryInterface.addIndex('KeyRequests', ['familyId', 'categoryId'], { transaction })
await queryInterface.addIndex('KeyRequests', ['familyId', 'senderDeviceId', 'deviceId', 'categoryId'], { transaction, unique: true })
})
}
export async function down (queryInterface: QueryInterface, sequelize: Sequelize) {
await sequelize.transaction({
type: Transaction.TYPES.EXCLUSIVE
}, async (transaction) => {
await queryInterface.dropTable('KeyRequests', { transaction })
})
}
@@ -0,0 +1,83 @@
/*
* server component for the TimeLimit App
* Copyright (C) 2019 - 2022 Jonas Lochmann
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { QueryInterface, Sequelize, Transaction } from 'sequelize'
export async function up (queryInterface: QueryInterface, sequelize: Sequelize) {
await sequelize.transaction({
type: Transaction.TYPES.EXCLUSIVE
}, async (transaction) => {
const dialect = sequelize.getDialect()
const isMysql = dialect === 'mysql' || dialect === 'mariadb'
const isPosgresql = dialect === 'postgres'
if (isMysql) {
await sequelize.query(
'CREATE TABLE `KeyResponses` (' +
'`familyId` VARCHAR(10) NOT NULL, ' +
'`receiverDeviceId` VARCHAR(6) NOT NULL, ' +
'`requestServerSequenceNumber` BIGINT NOT NULL, ' +
'`senderDeviceId` VARCHAR(6) NOT NULL, ' +
'`replyServerSequenceNumber` BIGINT NOT NULL, ' +
'`requestClientSequenceNumber` BIGINT NOT NULL, ' +
'`tempKey` BLOB NOT NULL, ' +
'`encryptedKey` BLOB NOT NULL, ' +
'`signature` BLOB NOT NULL, ' +
'PRIMARY KEY (`familyId`, `receiverDeviceId`, `requestServerSequenceNumber`, `senderDeviceId`), ' +
'FOREIGN KEY (`familyId`, `requestServerSequenceNumber`) REFERENCES `KeyRequests` (`familyId`, `serverSequenceNumber`) ON UPDATE CASCADE ON DELETE CASCADE ' +
')',
{ transaction }
)
} else {
await sequelize.query(
'CREATE TABLE "KeyResponses" (' +
'"familyId" VARCHAR(10) NOT NULL, ' +
'"receiverDeviceId" VARCHAR(6) NOT NULL, ' +
'"requestServerSequenceNumber" ' + (isPosgresql ? 'BIGINT' : 'LONG') + ' NOT NULL, ' +
'"senderDeviceId" VARCHAR(6) NOT NULL, ' +
'"replyServerSequenceNumber" ' + (isPosgresql ? 'BIGINT' : 'LONG') + ' NOT NULL, ' +
'"requestClientSequenceNumber" ' + (isPosgresql ? 'BIGINT' : 'LONG') + ' NOT NULL, ' +
'"tempKey" ' + (isPosgresql ? 'BYTEA' : 'BLOB') + ' NOT NULL, ' +
'"encryptedKey" ' + (isPosgresql ? 'BYTEA' : 'BLOB') + ' NOT NULL, ' +
'"signature" ' + (isPosgresql ? 'BYTEA' : 'BLOB') + ' NOT NULL, ' +
'PRIMARY KEY ("familyId", "receiverDeviceId", "requestServerSequenceNumber", "senderDeviceId"), ' +
'FOREIGN KEY ("familyId", "requestServerSequenceNumber") REFERENCES "KeyRequests" ("familyId", "serverSequenceNumber") ON UPDATE CASCADE ON DELETE CASCADE ' +
')',
{ transaction }
)
}
await queryInterface.addIndex('KeyResponses', ['familyId', 'requestServerSequenceNumber'], { transaction })
await queryInterface.addIndex(
'KeyResponses',
['familyId', 'receiverDeviceId', 'replyServerSequenceNumber'],
{
transaction,
unique: true,
name: 'key_response_index_fid_rdid_rssn'
}
)
})
}
export async function down (queryInterface: QueryInterface, sequelize: Sequelize) {
await sequelize.transaction({
type: Transaction.TYPES.EXCLUSIVE
}, async (transaction) => {
await queryInterface.dropTable('KeyResponses', { transaction })
})
}
@@ -0,0 +1,39 @@
/*
* server component for the TimeLimit App
* Copyright (C) 2019 - 2022 Jonas Lochmann
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { QueryInterface, Sequelize, Transaction } from 'sequelize'
import { attributesVersion14 as deviceAttributes } from '../../device'
export async function up (queryInterface: QueryInterface, sequelize: Sequelize) {
await sequelize.transaction({
type: Transaction.TYPES.EXCLUSIVE
}, async (transaction) => {
await queryInterface.addColumn('Devices', 'nextKeyReplySequenceNumber', {
...deviceAttributes.nextKeyReplySequenceNumber
}, {
transaction
})
})
}
export async function down (queryInterface: QueryInterface, sequelize: Sequelize) {
await sequelize.transaction({
type: Transaction.TYPES.EXCLUSIVE
}, async (transaction) => {
await queryInterface.removeColumn('Devices', 'nextKeyReplySequenceNumber', { transaction })
})
}
+3 -1
View File
@@ -64,5 +64,7 @@ export const prepareDeviceEntry = ({ familyId, userId, deviceAuthToken, deviceId
wasAsEnabled: false,
activityLevelBlocking: false,
isQorLater: false,
manipulationFlags: 0
manipulationFlags: 0,
publicKey: null,
nextKeyReplySequenceNumber: '1'
})
+3 -2
View File
@@ -1,6 +1,6 @@
/*
* server component for the TimeLimit App
* Copyright (C) 2019 - 2021 Jonas Lochmann
* Copyright (C) 2019 - 2022 Jonas Lochmann
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
@@ -65,7 +65,8 @@ export const createFamily = async ({ database, mailAuthToken, firstParentDevice,
deviceListVersion: generateVersionId(),
// 14 days demo version
fullVersionUntil: (Date.now() + 1000 * 60 * 60 * 24 * 14).toString(10),
hasFullVersion: true
hasFullVersion: true,
nextServerKeyRequestSeq: '1'
}, { transaction })
// create parent user
@@ -0,0 +1,34 @@
/*
* server component for the TimeLimit App
* Copyright (C) 2019 - 2022 Jonas Lochmann
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { FinishKeyRequestAction } from '../../../../action'
import { Cache } from '../cache'
export async function dispatchFinishKeyRequestAction ({ action, cache, deviceId }: {
deviceId: string
action: FinishKeyRequestAction
cache: Cache
}) {
await cache.database.keyRequest.destroy({
where: {
familyId: cache.familyId,
senderDeviceId: deviceId,
senderSequenceNumber: action.deviceSequenceNumber.toString(10)
},
transaction: cache.transaction
})
}
@@ -1,6 +1,6 @@
/*
* server component for the TimeLimit App
* Copyright (C) 2019 - 2020 Jonas Lochmann
* Copyright (C) 2019 - 2022 Jonas Lochmann
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
@@ -20,13 +20,18 @@ import {
AddUsedTimeAction,
AddUsedTimeActionVersion2,
AppLogicAction,
FinishKeyRequestAction,
ForceSyncAction,
MarkTaskPendingAction,
ReplyToKeyRequestAction,
RemoveInstalledAppsAction,
SendKeyRequestAction,
SignOutAtDeviceAction,
TriedDisablingDeviceAdminAction,
UpdateAppActivitiesAction,
UpdateDeviceStatusAction
UpdateDeviceStatusAction,
UpdateInstalledAppsAction,
UploadDevicePublicKeyAction
} from '../../../../action'
import { EventHandler } from '../../../../monitoring/eventhandler'
import { Cache } from '../cache'
@@ -34,13 +39,18 @@ import { ActionObjectTypeNotHandledException } from '../exception/illegal-state'
import { dispatchAddInstalledApps } from './addinstalledapps'
import { dispatchAddUsedTime } from './addusedtime'
import { dispatchAddUsedTimeVersion2 } from './addusedtime2'
import { dispatchFinishKeyRequestAction } from './finishkeyrequest'
import { dispatchForceSyncAction } from './forcesync'
import { dispatchMarkTaskPendingAction } from './marktaskpendingaction'
import { dispatchReplyToKeyRequestAction } from './replytokeyrequest'
import { dispatchRemoveInstalledApps } from './removeinstalledapps'
import { dispatchSendKeyRequestAction } from './sendkeyrequest'
import { dispatchSignOutAtDevice } from './signoutatdevice'
import { dispatchTriedDisablingDeviceAdmin } from './trieddisablingdeviceadmin'
import { dispatchUpdateAppActivities } from './updateappactivities'
import { dispatchUpdateDeviceStatus } from './updatedevicestatus'
import { dispatchUpdateInstalledApps } from './updateinstalledapps'
import { dispatchUploadDevicePublicKeyAction } from './uploaddevicepublickey'
export const dispatchAppLogicAction = async ({ action, deviceId, cache, eventHandler }: {
action: AppLogicAction
@@ -54,12 +64,18 @@ export const dispatchAppLogicAction = async ({ action, deviceId, cache, eventHan
await dispatchAddUsedTime({ deviceId, action, cache })
} else if (action instanceof AddUsedTimeActionVersion2) {
await dispatchAddUsedTimeVersion2({ deviceId, action, cache, eventHandler })
} else if (action instanceof FinishKeyRequestAction) {
await dispatchFinishKeyRequestAction({ deviceId, action, cache })
} else if (action instanceof ForceSyncAction) {
await dispatchForceSyncAction({ deviceId, action, cache })
} else if (action instanceof MarkTaskPendingAction) {
await dispatchMarkTaskPendingAction({ deviceId, action, cache })
} else if (action instanceof ReplyToKeyRequestAction) {
await dispatchReplyToKeyRequestAction({ deviceId, action, cache, eventHandler })
} else if (action instanceof RemoveInstalledAppsAction) {
await dispatchRemoveInstalledApps({ deviceId, action, cache })
} else if (action instanceof SendKeyRequestAction) {
await dispatchSendKeyRequestAction({ deviceId, action, cache })
} else if (action instanceof SignOutAtDeviceAction) {
await dispatchSignOutAtDevice({ deviceId, action, cache })
} else if (action instanceof UpdateDeviceStatusAction) {
@@ -68,6 +84,10 @@ export const dispatchAppLogicAction = async ({ action, deviceId, cache, eventHan
await dispatchUpdateAppActivities({ deviceId, action, cache })
} else if (action instanceof TriedDisablingDeviceAdminAction) {
await dispatchTriedDisablingDeviceAdmin({ deviceId, action, cache })
} else if (action instanceof UpdateInstalledAppsAction) {
await dispatchUpdateInstalledApps({ deviceId, action, cache })
} else if (action instanceof UploadDevicePublicKeyAction) {
await dispatchUploadDevicePublicKeyAction({ deviceId, action, cache, eventHandler })
} else {
throw new ActionObjectTypeNotHandledException()
}
@@ -0,0 +1,108 @@
/*
* server component for the TimeLimit App
* Copyright (C) 2019 - 2022 Jonas Lochmann
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { ReplyToKeyRequestAction } from '../../../../action'
import { Cache } from '../cache'
import { EventHandler } from '../../../../monitoring/eventhandler'
import { SourceDeviceNotFoundException } from '../exception/illegal-state'
export async function dispatchReplyToKeyRequestAction ({ deviceId, action, cache, eventHandler }: {
deviceId: string
action: ReplyToKeyRequestAction
cache: Cache
eventHandler: EventHandler
}) {
const requestUnsafe = await cache.database.keyRequest.findOne({
where: {
familyId: cache.familyId,
serverSequenceNumber: action.requestServerSequenceNumber.toString(10)
},
attributes: ['senderDeviceId', 'senderSequenceNumber'],
transaction: cache.transaction
})
if (!requestUnsafe) {
eventHandler.countEvent('dispatchReplyToKeyRequestAction:request does not exists (anymore)')
return
}
const request = {
senderDeviceId: requestUnsafe.senderDeviceId,
senderSequenceNumber: requestUnsafe.senderSequenceNumber
}
const oldReplyCounter = await cache.database.keyResponse.count({
where: {
familyId: cache.familyId,
receiverDeviceId: request.senderDeviceId,
requestServerSequenceNumber: action.requestServerSequenceNumber.toString(10),
senderDeviceId: deviceId
},
transaction: cache.transaction
})
if (oldReplyCounter !== 0) {
eventHandler.countEvent('dispatchReplyToKeyRequestAction:got duplicate reply which was ignored')
return
}
const deviceEntryUnsafe = await cache.database.device.findOne({
where: {
familyId: cache.familyId,
deviceId
},
transaction: cache.transaction,
attributes: ['nextKeyReplySequenceNumber']
})
if (!deviceEntryUnsafe) {
throw new SourceDeviceNotFoundException()
}
const deviceEntry = {
nextKeyReplySequenceNumber: deviceEntryUnsafe.nextKeyReplySequenceNumber
}
await cache.database.device.update({
nextKeyReplySequenceNumber: (parseInt(deviceEntry.nextKeyReplySequenceNumber) + 1).toString(10)
}, {
where: {
familyId: cache.familyId,
deviceId
},
transaction: cache.transaction
})
await cache.database.keyResponse.create({
familyId: cache.familyId,
receiverDeviceId: request.senderDeviceId,
requestServerSequenceNumber: action.requestServerSequenceNumber.toString(10),
senderDeviceId: deviceId,
replyServerSequenceNumber: deviceEntry.nextKeyReplySequenceNumber,
requestClientSequenceNumber: requestUnsafe.senderSequenceNumber,
tempKey: action.tempKey,
encryptedKey: action.encryptedKey,
signature: action.signature
}, {
transaction: cache.transaction
})
// there is no way (yet) to inform the specific device only
cache.areChangesImportant = true
}
@@ -0,0 +1,76 @@
/*
* server component for the TimeLimit App
* Copyright (C) 2019 - 2022 Jonas Lochmann
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { SendKeyRequestAction } from '../../../../action'
import { Cache } from '../cache'
import { SourceFamilyNotFoundException } from '../exception/illegal-state'
export async function dispatchSendKeyRequestAction ({ action, cache, deviceId }: {
deviceId: string
action: SendKeyRequestAction
cache: Cache
}) {
const familyEntryUnsafe = await cache.database.family.findOne({
where: {
familyId: cache.familyId
},
transaction: cache.transaction,
attributes: ['nextServerKeyRequestSeq']
})
if (!familyEntryUnsafe) {
throw new SourceFamilyNotFoundException()
}
const serverSequenceNumber = familyEntryUnsafe.nextServerKeyRequestSeq
await cache.database.family.update({
nextServerKeyRequestSeq: (parseInt(serverSequenceNumber, 10) + 1).toString(10)
}, {
where: {
familyId: cache.familyId
},
transaction: cache.transaction
})
await cache.database.keyRequest.destroy({
where: {
familyId: cache.familyId,
senderDeviceId: deviceId,
type: action.type,
deviceId: action.deviceId || null,
categoryId: action.categoryId || null
},
transaction: cache.transaction
})
await cache.database.keyRequest.create({
familyId: cache.familyId,
serverSequenceNumber: serverSequenceNumber,
senderDeviceId: deviceId,
senderSequenceNumber: action.deviceSequenceNumber.toString(10),
deviceId: action.deviceId || null,
categoryId: action.categoryId || null,
type: action.type,
tempKey: action.tempKey,
signature: action.signature
}, {
transaction: cache.transaction
})
cache.areChangesImportant = true
}
@@ -0,0 +1,59 @@
/*
* server component for the TimeLimit App
* Copyright (C) 2019 - 2022 Jonas Lochmann
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { UpdateInstalledAppsAction } from '../../../../action'
import { types } from '../../../../database/encryptedapplist'
import { generateVersionId } from '../../../../util/token'
import { Cache } from '../cache'
export async function dispatchUpdateInstalledApps ({ deviceId, action, cache }: {
deviceId: string
action: UpdateInstalledAppsAction
cache: Cache
}) {
if (action.base) {
await cache.database.encryptedAppList.upsert({
familyId: cache.familyId,
deviceId,
type: types.base,
version: generateVersionId(),
data: action.base
}, { transaction: cache.transaction })
}
if (action.diff) {
await cache.database.encryptedAppList.upsert({
familyId: cache.familyId,
deviceId,
type: types.diff,
version: generateVersionId(),
data: action.diff
}, { transaction: cache.transaction })
}
if (action.wipe) {
await cache.database.app.destroy({
where: {
familyId: cache.familyId,
deviceId
},
transaction: cache.transaction
})
cache.devicesWithModifiedInstalledApps.add(deviceId)
}
}
@@ -0,0 +1,50 @@
/*
* server component for the TimeLimit App
* Copyright (C) 2019 - 2022 Jonas Lochmann
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { UploadDevicePublicKeyAction } from '../../../../action'
import { Cache } from '../cache'
import { EventHandler } from '../../../../monitoring/eventhandler'
import { SourceDeviceNotFoundException } from '../exception/illegal-state'
export async function dispatchUploadDevicePublicKeyAction ({ deviceId, action, cache, eventHandler }: {
deviceId: string
action: UploadDevicePublicKeyAction
cache: Cache
eventHandler: EventHandler
}) {
const deviceEntry = await cache.database.device.findOne({
where: {
familyId: cache.familyId,
deviceId
},
transaction: cache.transaction
})
if (deviceEntry === null) {
throw new SourceDeviceNotFoundException()
} else if (deviceEntry.publicKey === null) {
deviceEntry.publicKey = action.key
await deviceEntry.save({ transaction: cache.transaction })
cache.invalidiateDeviceList = true
} else if (deviceEntry.publicKey.equals(action.key)) {
eventHandler.countEvent('dispatchUploadDevicePublicKeyAction:duplicate action')
} else {
eventHandler.countEvent('dispatchUploadDevicePublicKeyAction:got new public key for existing device')
}
}
@@ -0,0 +1,119 @@
/*
* server component for the TimeLimit App
* Copyright (C) 2019 - 2022 Jonas Lochmann
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import * as Sequelize from 'sequelize'
import { Database } from '../../../database'
import { ClientDataStatusDevicesExtended } from '../../../object/clientdatastatus'
import { ServerExtendedDeviceData, ServerCryptContainer } from '../../../object/serverdatastatus'
import { FamilyEntry } from './family-entry'
import { types } from '../../../database/encryptedapplist'
export async function getDeviceDetailList ({ database, transaction, familyEntry, devicesDetail }: {
database: Database
transaction: Sequelize.Transaction
familyEntry: FamilyEntry
devicesDetail: ClientDataStatusDevicesExtended
}): Promise<Array<ServerExtendedDeviceData> | null> {
const serverEncryptedAppsVersions = (await database.encryptedAppList.findAll({
where: {
familyId: familyEntry.familyId,
},
attributes: ['deviceId', 'type', 'version'],
transaction
})).map((item) => ({
deviceId: item.deviceId,
type: item.type,
version: item.version
}))
const devicesWithChangedBaseApps: Array<string> = []
const devicesWithChangedDiffApps: Array<string> = []
serverEncryptedAppsVersions.forEach((item) => {
if (item.type === types.base) {
if (!devicesDetail[item.deviceId] || devicesDetail[item.deviceId].appsB !== item.version) {
devicesWithChangedBaseApps.push(item.deviceId)
}
} else if (item.type === types.diff) {
if (!devicesDetail[item.deviceId] || devicesDetail[item.deviceId].appsD !== item.version) {
devicesWithChangedDiffApps.push(item.deviceId)
}
}
})
const updatedDeviceIds = Array.from(new Set([...devicesWithChangedBaseApps, ...devicesWithChangedDiffApps]))
if (updatedDeviceIds.length === 0) return null
const updatedBaseApps = devicesWithChangedBaseApps.length === 0 ? [] : (await database.encryptedAppList.findAll({
where: {
familyId: familyEntry.familyId,
deviceId: {
[Sequelize.Op.in]: devicesWithChangedBaseApps
},
type: types.base
},
attributes: [
'deviceId',
'version',
'data'
],
transaction
})).map((item) => ({
deviceId: item.deviceId,
version: item.version,
data: item.data
}))
const updatedDiffApps = devicesWithChangedDiffApps.length === 0 ? [] : (await database.encryptedAppList.findAll({
where: {
familyId: familyEntry.familyId,
deviceId: {
[Sequelize.Op.in]: devicesWithChangedDiffApps
},
type: types.diff
},
attributes: [
'deviceId',
'version',
'data'
],
transaction
})).map((item) => ({
deviceId: item.deviceId,
version: item.version,
data: item.data
}))
return updatedDeviceIds.map((deviceId) => {
const appsBase = updatedBaseApps.find((item) => item.deviceId === deviceId)
const appsDiff = updatedDiffApps.find((item) => item.deviceId === deviceId)
return {
deviceId,
appsBase: appsBase ? wrapServerCryptContainer(appsBase) : undefined,
appsDiff: appsDiff ? wrapServerCryptContainer(appsDiff) : undefined
}
})
}
function wrapServerCryptContainer({ version, data }: { version: string, data: Buffer }): ServerCryptContainer {
return {
version,
data: data.toString('base64')
}
}
@@ -65,7 +65,8 @@ export async function getDeviceList ({ database, transaction, familyEntry }: {
wasAsEnabled: item.wasAsEnabled,
activityLevelBlocking: item.activityLevelBlocking,
qOrLater: item.isQorLater,
mFlags: item.manipulationFlags
mFlags: item.manipulationFlags,
pk: item.publicKey ? item.publicKey.toString('base64') : undefined
}))
}
}
@@ -26,25 +26,32 @@ import {
getCategoryAssignedApps, getCategoryBaseDatas, getCategoryDataToSync,
getRules, getTasks, getUsedTimes
} from './category'
import { getDeviceDetailList } from './device-detail'
import { getDeviceList } from './device-list'
import { getFamilyEntry } from './family-entry'
import { getUserList } from './user-list'
import { getKeyRequests } from './key-requests'
import { getKeyResponses } from './key-responses'
export const generateServerDataStatus = async ({ database, clientStatus, familyId, transaction }: {
export const generateServerDataStatus = async ({
database, clientStatus, familyId, deviceId, transaction
}: {
database: Database
clientStatus: ClientDataStatus
familyId: string
deviceId: string
transaction: Sequelize.Transaction
}): Promise<ServerDataStatus> => {
const familyEntry = await getFamilyEntry({ database, familyId, transaction })
const doesClientSupportTasks = clientStatus.clientLevel !== undefined && clientStatus.clientLevel >= 3
const doesClientSupportCryptoApps = clientStatus.clientLevel !== undefined && clientStatus.clientLevel >= 4
const result: ServerDataStatus = {
fullVersion: config.alwaysPro ? 1 : (
familyEntry.hasFullVersion ? parseInt(familyEntry.fullVersionUntil, 10) : 0
),
message: await getStatusMessage({ database, transaction }) || undefined,
apiLevel: 3
apiLevel: 4
}
if (familyEntry.deviceListVersion !== clientStatus.devices) {
@@ -103,5 +110,30 @@ export const generateServerDataStatus = async ({ database, clientStatus, familyI
})
}
if (doesClientSupportCryptoApps) {
result.devices2 = await getDeviceDetailList({
database,
transaction,
familyEntry,
devicesDetail: clientStatus.devicesDetail || {}
}) || undefined
result.krq = await getKeyRequests({
database,
transaction,
familyEntry,
deviceId,
lastSeenRequestIndex: clientStatus.kri || null
}) || undefined
result.kr = await getKeyResponses({
database,
transaction,
familyEntry,
deviceId,
lastSeenRequestIndex: clientStatus.kr || null
}) || undefined
}
return result
}
@@ -0,0 +1,58 @@
/*
* server component for the TimeLimit App
* Copyright (C) 2019 - 2022 Jonas Lochmann
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import * as Sequelize from 'sequelize'
import { Database } from '../../../database'
import { ServerKeyRequest } from '../../../object/serverdatastatus'
import { FamilyEntry } from './family-entry'
export async function getKeyRequests ({ database, transaction, familyEntry, lastSeenRequestIndex, deviceId }: {
database: Database
transaction: Sequelize.Transaction
familyEntry: FamilyEntry
lastSeenRequestIndex: number | null
deviceId: string
}): Promise<Array<ServerKeyRequest> | null> {
const data = await database.keyRequest.findAll({
where: {
familyId: familyEntry.familyId,
senderDeviceId: {
[Sequelize.Op.ne]: deviceId
},
...(lastSeenRequestIndex === null ? {} : {
serverSequenceNumber: {
[Sequelize.Op.gt]: lastSeenRequestIndex
}
})
},
transaction,
limit: 32
})
if (data.length === 0) return null
return data.map((item) => ({
srvSeq: parseInt(item.serverSequenceNumber),
senId: item.senderDeviceId,
senSeq: parseInt(item.senderSequenceNumber),
deviceId: item.deviceId || undefined,
categoryId: item.categoryId || undefined,
type: item.type,
tempKey: item.tempKey.toString('base64'),
signature: item.signature.toString('base64')
}))
}
@@ -0,0 +1,63 @@
/*
* server component for the TimeLimit App
* Copyright (C) 2019 - 2022 Jonas Lochmann
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import * as Sequelize from 'sequelize'
import { Database } from '../../../database'
import { ServerKeyResponse } from '../../../object/serverdatastatus'
import { FamilyEntry } from './family-entry'
export async function getKeyResponses ({ database, transaction, familyEntry, lastSeenRequestIndex, deviceId }: {
database: Database
transaction: Sequelize.Transaction
familyEntry: FamilyEntry
lastSeenRequestIndex: number | null
deviceId: string
}): Promise<Array<ServerKeyResponse> | null> {
if (lastSeenRequestIndex !== null) {
await database.keyResponse.destroy({
where: {
familyId: familyEntry.familyId,
receiverDeviceId: deviceId,
replyServerSequenceNumber: {
[Sequelize.Op.lte]: lastSeenRequestIndex.toString(10)
}
},
transaction
})
}
const data = await database.keyResponse.findAll({
where: {
familyId: familyEntry.familyId,
receiverDeviceId: deviceId,
},
order: [['replyServerSequenceNumber', 'ASC']],
transaction,
limit: 32
})
if (data.length === 0) return null
return data.map((item) => ({
srvSeq: parseInt(item.replyServerSequenceNumber),
sender: item.senderDeviceId,
rqSeq: parseInt(item.requestClientSequenceNumber),
tempKey: item.tempKey.toString('base64'),
cryptKey: item.encryptedKey.toString('base64'),
signature: item.signature.toString('base64')
}))
}
+10 -1
View File
@@ -1,6 +1,6 @@
/*
* server component for the TimeLimit App
* Copyright (C) 2019 - 2020 Jonas Lochmann
* Copyright (C) 2019 - 2022 Jonas Lochmann
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
@@ -21,10 +21,14 @@ export interface ClientDataStatus {
categories: ClientDataStatusCategories
users: string // userListVersion
clientLevel?: number
devicesDetail?: ClientDataStatusDevicesExtended
kri?: number // last key request index
kr?: number // last key response index
}
export type ClientDataStatusApps = {[key: string]: string} // installedAppsVersionsByDeviceId
export type ClientDataStatusCategories = {[key: string]: CategoryDataStatus}
export type ClientDataStatusDevicesExtended = {[key: string]: DeviceDataStatus}
export interface CategoryDataStatus {
base: string // baseVersion
@@ -33,3 +37,8 @@ export interface CategoryDataStatus {
usedTime: string // usedTimeItemsVersion
tasks?: string // taskListVersion
}
export interface DeviceDataStatus {
appsB?: string // encrypted app list base version
appsD?: string // encrypted app list diff version
}
+35
View File
@@ -23,6 +23,7 @@ import { RuntimePermissionStatus } from '../model/runtimepermissionstatus'
export interface ServerDataStatus {
devices?: ServerDeviceList // newDeviceList
devices2?: Array<ServerExtendedDeviceData> // updatedExtendedDeviceData
apps?: Array<ServerInstalledAppsData> // newInstalledApps
rmCategories?: Array<string> // removedCategories
categoryBase?: Array<ServerUpdatedCategoryBaseData> // newCategoryBaseData
@@ -31,6 +32,8 @@ export interface ServerDataStatus {
rules?: Array<ServerUpdatedTimeLimitRules> // newOrUpdatedTimeLimitRules
tasks?: Array<ServerUpdatedCategoryTasks> // newOrUpdatedTasks
users?: ServerUserList // newUserList
krq?: Array<ServerKeyRequest> // pendingKeyRequests
kr?: Array<ServerKeyResponse> // keyResponses
fullVersion: number // fullVersionUntil
message?: string
apiLevel: number
@@ -97,6 +100,7 @@ export interface ServerDeviceData {
activityLevelBlocking: boolean
qOrLater: boolean
mFlags: number // manipulation flags
pk?: string // public key
}
export interface ServerUpdatedCategoryBaseData {
@@ -217,3 +221,34 @@ export interface ServerInstalledAppsData {
apps: Array<SerializedInstalledApp>
activities: Array<SerializedAppActivityItem>
}
export interface ServerExtendedDeviceData {
deviceId: string
appsBase?: ServerCryptContainer
appsDiff?: ServerCryptContainer
}
export interface ServerCryptContainer {
version: string
data: string
}
export interface ServerKeyRequest {
srvSeq: number
senId: string
senSeq: number
deviceId?: string
categoryId?: string
type: number
tempKey: string
signature: string
}
export interface ServerKeyResponse {
srvSeq: number
sender: string
rqSeq: number
tempKey: string,
cryptKey: string,
signature: string
}