Merge pull request #2387 from uhoreg/key_backup_spec
initial version of spec for key backups
This commit is contained in:
commit
cf37688b7c
4 changed files with 1124 additions and 0 deletions
50
api/client-server/definitions/key_backup_data.yaml
Normal file
50
api/client-server/definitions/key_backup_data.yaml
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
# Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
type: object
|
||||||
|
title: KeyBackupData
|
||||||
|
description: "The key data"
|
||||||
|
properties:
|
||||||
|
first_message_index:
|
||||||
|
description: |-
|
||||||
|
The index of the first message in the session that the key can decrypt.
|
||||||
|
type: integer
|
||||||
|
example: 1
|
||||||
|
forwarded_count:
|
||||||
|
description: |-
|
||||||
|
The number of times this key has been forwarded via key-sharing between devices.
|
||||||
|
type: integer
|
||||||
|
example: 0
|
||||||
|
is_verified:
|
||||||
|
description: |-
|
||||||
|
Whether the device backing up the key verified the device that the key
|
||||||
|
is from.
|
||||||
|
type: boolean
|
||||||
|
example: false
|
||||||
|
session_data:
|
||||||
|
description: |-
|
||||||
|
Algorithm-dependent data. See the documentation for the backup
|
||||||
|
algorithms in `Server-side key backups`_ for more information on the
|
||||||
|
expected format of the data.
|
||||||
|
type: object
|
||||||
|
example: {
|
||||||
|
"ephemeral": "base64+ephemeral+key",
|
||||||
|
"ciphertext": "base64+ciphertext+of+JSON+data",
|
||||||
|
"mac": "base64+mac+of+ciphertext"
|
||||||
|
}
|
||||||
|
required:
|
||||||
|
- first_message_index
|
||||||
|
- forwarded_count
|
||||||
|
- is_verified
|
||||||
|
- session_data
|
940
api/client-server/key_backup.yaml
Normal file
940
api/client-server/key_backup.yaml
Normal file
|
@ -0,0 +1,940 @@
|
||||||
|
# Copyright 2019-2020 The Matrix.org Foundation C.I.C.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
swagger: '2.0'
|
||||||
|
info:
|
||||||
|
title: "Matrix Client-Server Key Backup API"
|
||||||
|
version: "1.0.0"
|
||||||
|
host: localhost:8008
|
||||||
|
schemes:
|
||||||
|
- https
|
||||||
|
- http
|
||||||
|
basePath: /_matrix/client/%CLIENT_MAJOR_VERSION%
|
||||||
|
consumes:
|
||||||
|
- application/json
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
securityDefinitions:
|
||||||
|
$ref: definitions/security.yaml
|
||||||
|
paths:
|
||||||
|
"/room_keys/version":
|
||||||
|
post:
|
||||||
|
summary: Create a new backup.
|
||||||
|
description: |-
|
||||||
|
Creates a new backup.
|
||||||
|
operationId: postRoomKeysVersion
|
||||||
|
security:
|
||||||
|
- accessToken: []
|
||||||
|
parameters:
|
||||||
|
- in: body
|
||||||
|
name: version
|
||||||
|
description: "The backup configuration."
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
algorithm:
|
||||||
|
description: The algorithm used for storing backups.
|
||||||
|
type: string
|
||||||
|
enum: ["m.megolm_backup.v1.curve25519-aes-sha2"]
|
||||||
|
example: "m.megolm_backup.v1.curve25519-aes-sha2"
|
||||||
|
auth_data:
|
||||||
|
description: |-
|
||||||
|
Algorithm-dependent data. See the documentation for the backup
|
||||||
|
algorithms in `Server-side key backups`_ for more information on the
|
||||||
|
expected format of the data.
|
||||||
|
type: object
|
||||||
|
example: {
|
||||||
|
"public_key": "abcdefg",
|
||||||
|
"signatures": {
|
||||||
|
"@alice:example.org": {
|
||||||
|
"ed25519:deviceid": "signature"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
required:
|
||||||
|
- algorithm
|
||||||
|
- auth_data
|
||||||
|
responses:
|
||||||
|
200:
|
||||||
|
description:
|
||||||
|
The version id of the new backup.
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
version:
|
||||||
|
type: string
|
||||||
|
description: The backup version. This is an opaque string.
|
||||||
|
example: "1"
|
||||||
|
required:
|
||||||
|
- version
|
||||||
|
429:
|
||||||
|
description: This request was rate-limited.
|
||||||
|
schema:
|
||||||
|
"$ref": "definitions/errors/rate_limited.yaml"
|
||||||
|
tags:
|
||||||
|
- End-to-end encryption
|
||||||
|
get:
|
||||||
|
summary: Get information about the latest backup version.
|
||||||
|
description: |-
|
||||||
|
Get information about the latest backup version.
|
||||||
|
operationId: getRoomKeysVersionCurrent
|
||||||
|
security:
|
||||||
|
- accessToken: []
|
||||||
|
responses:
|
||||||
|
200:
|
||||||
|
description:
|
||||||
|
The information about the backup.
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
algorithm:
|
||||||
|
type: string
|
||||||
|
description: The algorithm used for storing backups.
|
||||||
|
enum: ["m.megolm_backup.v1.curve25519-aes-sha2"]
|
||||||
|
example: "m.megolm_backup.v1.curve25519-aes-sha2"
|
||||||
|
auth_data:
|
||||||
|
description: |-
|
||||||
|
Algorithm-dependent data. See the documentation for the backup
|
||||||
|
algorithms in `Server-side key backups`_ for more information on the
|
||||||
|
expected format of the data.
|
||||||
|
type: object
|
||||||
|
example: {
|
||||||
|
"public_key": "abcdefg",
|
||||||
|
"signatures": {
|
||||||
|
"@alice:example.org": {
|
||||||
|
"ed25519:deviceid": "signature"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
count:
|
||||||
|
description: The number of keys stored in the backup.
|
||||||
|
type: integer
|
||||||
|
example: 42
|
||||||
|
etag:
|
||||||
|
description: |-
|
||||||
|
An opaque string representing stored keys in the backup.
|
||||||
|
Clients can compare it with the ``etag`` value they received
|
||||||
|
in the request of their last key storage request. If not
|
||||||
|
equal, another client has modified the backup.
|
||||||
|
type: string
|
||||||
|
example: "anopaquestring"
|
||||||
|
version:
|
||||||
|
type: string
|
||||||
|
description: The backup version
|
||||||
|
example: "1"
|
||||||
|
required:
|
||||||
|
- algorithm
|
||||||
|
- auth_data
|
||||||
|
- count
|
||||||
|
- etag
|
||||||
|
- version
|
||||||
|
404:
|
||||||
|
description:
|
||||||
|
No backup exists.
|
||||||
|
examples:
|
||||||
|
application/json: {
|
||||||
|
"errcode": "M_NOT_FOUND",
|
||||||
|
"error": "No current backup version"
|
||||||
|
}
|
||||||
|
schema:
|
||||||
|
"$ref": "definitions/errors/error.yaml"
|
||||||
|
429:
|
||||||
|
description: This request was rate-limited.
|
||||||
|
schema:
|
||||||
|
"$ref": "definitions/errors/rate_limited.yaml"
|
||||||
|
tags:
|
||||||
|
- End-to-end encryption
|
||||||
|
"/room_keys/version/{version}":
|
||||||
|
get:
|
||||||
|
summary: Get information about an existing backup.
|
||||||
|
description: |-
|
||||||
|
Get information about an existing backup.
|
||||||
|
operationId: getRoomKeysVersion
|
||||||
|
security:
|
||||||
|
- accessToken: []
|
||||||
|
parameters:
|
||||||
|
- in: path
|
||||||
|
type: string
|
||||||
|
name: version
|
||||||
|
description: |-
|
||||||
|
The backup version to get, as returned in the ``version`` parameter
|
||||||
|
of the response in `POST /_matrix/client/r0/room_keys/version`_ or
|
||||||
|
this endpoint.
|
||||||
|
required: true
|
||||||
|
x-example: "1"
|
||||||
|
responses:
|
||||||
|
200:
|
||||||
|
description:
|
||||||
|
The information about the requested backup.
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
algorithm:
|
||||||
|
type: string
|
||||||
|
description: The algorithm used for storing backups.
|
||||||
|
enum: ["m.megolm_backup.v1.curve25519-aes-sha2"]
|
||||||
|
example: "m.megolm_backup.v1.curve25519-aes-sha2"
|
||||||
|
auth_data:
|
||||||
|
description: |-
|
||||||
|
Algorithm-dependent data. See the documentation for the backup
|
||||||
|
algorithms in `Server-side key backups`_ for more information on the
|
||||||
|
expected format of the data.
|
||||||
|
type: object
|
||||||
|
example: {
|
||||||
|
"public_key": "abcdefg",
|
||||||
|
"signatures": {
|
||||||
|
"@alice:example.org": {
|
||||||
|
"ed25519:deviceid": "signature"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
count:
|
||||||
|
description: The number of keys stored in the backup.
|
||||||
|
type: integer
|
||||||
|
example: 42
|
||||||
|
etag:
|
||||||
|
description: |-
|
||||||
|
An opaque string representing stored keys in the backup.
|
||||||
|
Clients can compare it with the ``etag`` value they received
|
||||||
|
in the request of their last key storage request. If not
|
||||||
|
equal, another client has modified the backup.
|
||||||
|
type: string
|
||||||
|
example: "anopaquestring"
|
||||||
|
version:
|
||||||
|
type: string
|
||||||
|
description: The backup version
|
||||||
|
example: "1"
|
||||||
|
required:
|
||||||
|
- algorithm
|
||||||
|
- auth_data
|
||||||
|
- count
|
||||||
|
- etag
|
||||||
|
- version
|
||||||
|
404:
|
||||||
|
description:
|
||||||
|
The backup specified does not exist.
|
||||||
|
examples:
|
||||||
|
application/json: {
|
||||||
|
"errcode": "M_NOT_FOUND",
|
||||||
|
"error": "Unknown backup version"
|
||||||
|
}
|
||||||
|
schema:
|
||||||
|
"$ref": "definitions/errors/error.yaml"
|
||||||
|
429:
|
||||||
|
description: This request was rate-limited.
|
||||||
|
schema:
|
||||||
|
"$ref": "definitions/errors/rate_limited.yaml"
|
||||||
|
tags:
|
||||||
|
- End-to-end encryption
|
||||||
|
put:
|
||||||
|
summary: Update information about an existing backup.
|
||||||
|
description: |-
|
||||||
|
Update information about an existing backup. Only ``auth_data`` can be modified.
|
||||||
|
operationId: putRoomKeysVersion
|
||||||
|
security:
|
||||||
|
- accessToken: []
|
||||||
|
parameters:
|
||||||
|
- in: path
|
||||||
|
type: string
|
||||||
|
name: version
|
||||||
|
description: |-
|
||||||
|
The backup version to update, as returned in the ``version``
|
||||||
|
parameter in the response of `POST
|
||||||
|
/_matrix/client/r0/room_keys/version`_ or `GET
|
||||||
|
/_matrix/client/r0/room_keys/version/{version}`_.
|
||||||
|
required: true
|
||||||
|
x-example: "1"
|
||||||
|
- in: body
|
||||||
|
name: version
|
||||||
|
description: "The backup configuration"
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
algorithm:
|
||||||
|
description: |-
|
||||||
|
The algorithm used for storing backups. Must be the same as
|
||||||
|
the algorithm currently used by the backup.
|
||||||
|
type: string
|
||||||
|
enum: ["m.megolm_backup.v1.curve25519-aes-sha2"]
|
||||||
|
example: "m.megolm_backup.v1.curve25519-aes-sha2"
|
||||||
|
auth_data:
|
||||||
|
description: |-
|
||||||
|
Algorithm-dependent data. See the documentation for the backup
|
||||||
|
algorithms in `Server-side key backups`_ for more information on the
|
||||||
|
expected format of the data.
|
||||||
|
type: object
|
||||||
|
example: {
|
||||||
|
"public_key": "abcdefg",
|
||||||
|
"signatures": {
|
||||||
|
"@alice:example.org": {
|
||||||
|
"ed25519:deviceid": "signature"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
version:
|
||||||
|
description: |-
|
||||||
|
The backup version. If present, must be the same as the
|
||||||
|
version in the path parameter.
|
||||||
|
type: string
|
||||||
|
example: "1"
|
||||||
|
required:
|
||||||
|
- algorithm
|
||||||
|
- auth_data
|
||||||
|
responses:
|
||||||
|
200:
|
||||||
|
description: The update succeeded.
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties: {}
|
||||||
|
400:
|
||||||
|
description: |-
|
||||||
|
A parameter was incorrect. For example, the ``algorithm`` does not
|
||||||
|
match the current backup algorithm, or the ``version`` in the body
|
||||||
|
does not match the ``version`` in the path.
|
||||||
|
examples:
|
||||||
|
application/json: {
|
||||||
|
"errcode": "M_INVALID_PARAM",
|
||||||
|
"error": "Algorithm does not match"
|
||||||
|
}
|
||||||
|
schema:
|
||||||
|
"$ref": "definitions/errors/error.yaml"
|
||||||
|
404:
|
||||||
|
description: The backup specified does not exist.
|
||||||
|
examples:
|
||||||
|
application/json: {
|
||||||
|
"errcode": "M_NOT_FOUND",
|
||||||
|
"error": "Unknown backup version"
|
||||||
|
}
|
||||||
|
schema:
|
||||||
|
"$ref": "definitions/errors/error.yaml"
|
||||||
|
429:
|
||||||
|
description: This request was rate-limited.
|
||||||
|
schema:
|
||||||
|
"$ref": "definitions/errors/rate_limited.yaml"
|
||||||
|
tags:
|
||||||
|
- End-to-end encryption
|
||||||
|
delete:
|
||||||
|
summary: Delete an existing key backup.
|
||||||
|
description: |-
|
||||||
|
Delete an existing key backup. Both the information about the backup,
|
||||||
|
as well as all key data related to the backup will be deleted.
|
||||||
|
operationId: deleteRoomKeysVersion
|
||||||
|
security:
|
||||||
|
- accessToken: []
|
||||||
|
parameters:
|
||||||
|
- in: path
|
||||||
|
type: string
|
||||||
|
name: version
|
||||||
|
description: |-
|
||||||
|
The backup version to delete, as returned in the ``version``
|
||||||
|
parameter in the response of `POST
|
||||||
|
/_matrix/client/r0/room_keys/version`_ or `GET
|
||||||
|
/_matrix/client/r0/room_keys/version/{version}`_.
|
||||||
|
required: true
|
||||||
|
x-example: "1"
|
||||||
|
responses:
|
||||||
|
200:
|
||||||
|
description: |-
|
||||||
|
The delete succeeded, or the specified backup was previously
|
||||||
|
deleted.
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties: {}
|
||||||
|
404:
|
||||||
|
description: |-
|
||||||
|
The backup specified does not exist. If the backup was previously
|
||||||
|
deleted, the call should succeed rather than returning an error.
|
||||||
|
examples:
|
||||||
|
application/json: {
|
||||||
|
"errcode": "M_NOT_FOUND",
|
||||||
|
"error": "Unknown backup version"
|
||||||
|
}
|
||||||
|
schema:
|
||||||
|
"$ref": "definitions/errors/error.yaml"
|
||||||
|
429:
|
||||||
|
description: This request was rate-limited.
|
||||||
|
schema:
|
||||||
|
"$ref": "definitions/errors/rate_limited.yaml"
|
||||||
|
tags:
|
||||||
|
- End-to-end encryption
|
||||||
|
"/room_keys/keys/{roomId}/{sessionId}":
|
||||||
|
put:
|
||||||
|
summary: Store a key in the backup.
|
||||||
|
description: |-
|
||||||
|
Store a key in the backup.
|
||||||
|
operationId: postRoomKeysKeyRoomIdSessionId
|
||||||
|
security:
|
||||||
|
- accessToken: []
|
||||||
|
parameters:
|
||||||
|
- in: query
|
||||||
|
type: string
|
||||||
|
name: version
|
||||||
|
description: |-
|
||||||
|
The backup in which to store the key. Must be the current backup.
|
||||||
|
required: true
|
||||||
|
x-example: "1"
|
||||||
|
- in: path
|
||||||
|
type: string
|
||||||
|
name: roomId
|
||||||
|
description: The ID of the room that the key is for.
|
||||||
|
required: true
|
||||||
|
x-example: "!roomid:example.org"
|
||||||
|
- in: path
|
||||||
|
type: string
|
||||||
|
name: sessionId
|
||||||
|
description: The ID of the megolm session that the key is for.
|
||||||
|
required: true
|
||||||
|
x-example: "sessionid"
|
||||||
|
- in: body
|
||||||
|
name: data
|
||||||
|
description: "The key data."
|
||||||
|
schema:
|
||||||
|
"$ref": "definitions/key_backup_data.yaml"
|
||||||
|
responses:
|
||||||
|
200:
|
||||||
|
description: The update succeeded.
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
etag:
|
||||||
|
description: |-
|
||||||
|
The new etag value representing stored keys in the backup.
|
||||||
|
See ``GET /room_keys/version/{version}`` for more details.
|
||||||
|
type: string
|
||||||
|
example: "abcdefg"
|
||||||
|
count:
|
||||||
|
description: The number of keys stored in the backup
|
||||||
|
type: integer
|
||||||
|
example: 10
|
||||||
|
required:
|
||||||
|
- etag
|
||||||
|
- count
|
||||||
|
403:
|
||||||
|
description: |-
|
||||||
|
The version specified does not match the current backup version.
|
||||||
|
The current version will be included in the ``current_version``
|
||||||
|
field.
|
||||||
|
examples:
|
||||||
|
application/json: {
|
||||||
|
"errcode": "M_WRONG_ROOM_KEYS_VERSION",
|
||||||
|
"error": "Wrong backup version.",
|
||||||
|
"current_version": "42"
|
||||||
|
}
|
||||||
|
schema:
|
||||||
|
"$ref": "definitions/errors/error.yaml"
|
||||||
|
429:
|
||||||
|
description: This request was rate-limited.
|
||||||
|
schema:
|
||||||
|
"$ref": "definitions/errors/rate_limited.yaml"
|
||||||
|
tags:
|
||||||
|
- End-to-end encryption
|
||||||
|
get:
|
||||||
|
summary: Retrieve a key from the backup
|
||||||
|
description: |-
|
||||||
|
Retrieve a key from the backup.
|
||||||
|
operationId: getRoomKeysKeyRoomIdSessionId
|
||||||
|
security:
|
||||||
|
- accessToken: []
|
||||||
|
parameters:
|
||||||
|
- in: query
|
||||||
|
type: string
|
||||||
|
name: version
|
||||||
|
description: |-
|
||||||
|
The backup from which to retrieve the key
|
||||||
|
required: true
|
||||||
|
x-example: "1"
|
||||||
|
- in: path
|
||||||
|
type: string
|
||||||
|
name: roomId
|
||||||
|
description: The ID of the room that the requested key is for.
|
||||||
|
required: true
|
||||||
|
x-example: "!roomid:example.org"
|
||||||
|
- in: path
|
||||||
|
type: string
|
||||||
|
name: sessionId
|
||||||
|
description: The ID of the megolm session whose key is requested.
|
||||||
|
required: true
|
||||||
|
x-example: "sessionid"
|
||||||
|
responses:
|
||||||
|
200:
|
||||||
|
description: The key data
|
||||||
|
schema:
|
||||||
|
"$ref": "definitions/key_backup_data.yaml"
|
||||||
|
404:
|
||||||
|
description: The key or backup was not found.
|
||||||
|
examples:
|
||||||
|
application/json: {
|
||||||
|
"errcode": "M_NOT_FOUND",
|
||||||
|
"error": "Key not found."
|
||||||
|
}
|
||||||
|
schema:
|
||||||
|
"$ref": "definitions/errors/error.yaml"
|
||||||
|
429:
|
||||||
|
description: This request was rate-limited.
|
||||||
|
schema:
|
||||||
|
"$ref": "definitions/errors/rate_limited.yaml"
|
||||||
|
delete:
|
||||||
|
summary: Delete a key from the backup
|
||||||
|
description: |-
|
||||||
|
Delete a key from the backup.
|
||||||
|
operationId: deleteRoomKeysKeyRoomIdSessionId
|
||||||
|
security:
|
||||||
|
- accessToken: []
|
||||||
|
parameters:
|
||||||
|
- in: query
|
||||||
|
type: string
|
||||||
|
name: version
|
||||||
|
description: |-
|
||||||
|
The backup from which to delete the key
|
||||||
|
required: true
|
||||||
|
x-example: "1"
|
||||||
|
- in: path
|
||||||
|
type: string
|
||||||
|
name: roomId
|
||||||
|
description: The ID of the room that the specified key is for.
|
||||||
|
required: true
|
||||||
|
x-example: "!roomid:example.org"
|
||||||
|
- in: path
|
||||||
|
type: string
|
||||||
|
name: sessionId
|
||||||
|
description: The ID of the megolm session whose key is to be deleted.
|
||||||
|
required: true
|
||||||
|
x-example: "sessionid"
|
||||||
|
responses:
|
||||||
|
200:
|
||||||
|
description: The update succeeded
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
etag:
|
||||||
|
description: |-
|
||||||
|
The new etag value representing stored keys in the backup.
|
||||||
|
See ``GET /room_keys/version/{version}`` for more details.
|
||||||
|
type: string
|
||||||
|
example: "abcdefg"
|
||||||
|
count:
|
||||||
|
description: The number of keys stored in the backup
|
||||||
|
type: integer
|
||||||
|
example: 10
|
||||||
|
required:
|
||||||
|
- etag
|
||||||
|
- count
|
||||||
|
404:
|
||||||
|
description: |-
|
||||||
|
The backup was not found.
|
||||||
|
examples:
|
||||||
|
application/json: {
|
||||||
|
"errcode": "M_NOT_FOUND",
|
||||||
|
"error": "Unknown backup version"
|
||||||
|
}
|
||||||
|
schema:
|
||||||
|
"$ref": "definitions/errors/error.yaml"
|
||||||
|
429:
|
||||||
|
description: This request was rate-limited.
|
||||||
|
schema:
|
||||||
|
"$ref": "definitions/errors/rate_limited.yaml"
|
||||||
|
"/room_keys/keys/{roomId}":
|
||||||
|
put:
|
||||||
|
summary: Store several keys in the backup for a given room.
|
||||||
|
description: |-
|
||||||
|
Store a key in the backup.
|
||||||
|
operationId: postRoomKeysKeyRoomId
|
||||||
|
security:
|
||||||
|
- accessToken: []
|
||||||
|
parameters:
|
||||||
|
- in: query
|
||||||
|
type: string
|
||||||
|
name: version
|
||||||
|
description: |-
|
||||||
|
The backup in which to store the keys. Must be the current backup.
|
||||||
|
required: true
|
||||||
|
x-example: "1"
|
||||||
|
- in: path
|
||||||
|
type: string
|
||||||
|
name: roomId
|
||||||
|
description: The ID of the room that the keys are for.
|
||||||
|
required: true
|
||||||
|
x-example: "!roomid:example.org"
|
||||||
|
- in: body
|
||||||
|
description: "The backup data"
|
||||||
|
name: backupData
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
sessions:
|
||||||
|
type: object
|
||||||
|
description: |-
|
||||||
|
A map of session IDs to key data.
|
||||||
|
additionalProperties:
|
||||||
|
allOf:
|
||||||
|
- $ref: "definitions/key_backup_data.yaml"
|
||||||
|
example: {
|
||||||
|
"sessionid1": {
|
||||||
|
"ephemeral": "base64+ephemeral+key",
|
||||||
|
"ciphertext": "base64+ciphertext+of+JSON+data",
|
||||||
|
"mac": "base64+mac+of+ciphertext"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
responses:
|
||||||
|
200:
|
||||||
|
description: The update succeeded
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
etag:
|
||||||
|
description: |-
|
||||||
|
The new etag value representing stored keys in the backup.
|
||||||
|
See ``GET /room_keys/version/{version}`` for more details.
|
||||||
|
type: string
|
||||||
|
example: "abcdefg"
|
||||||
|
count:
|
||||||
|
description: The number of keys stored in the backup
|
||||||
|
type: integer
|
||||||
|
example: 10
|
||||||
|
required:
|
||||||
|
- etag
|
||||||
|
- count
|
||||||
|
403:
|
||||||
|
description: |-
|
||||||
|
The version specified does not match the current backup version.
|
||||||
|
The current version will be included in the ``current_version``
|
||||||
|
field.
|
||||||
|
examples:
|
||||||
|
application/json: {
|
||||||
|
"errcode": "M_WRONG_ROOM_KEYS_VERSION",
|
||||||
|
"error": "Wrong backup version.",
|
||||||
|
"current_version": "42"
|
||||||
|
}
|
||||||
|
schema:
|
||||||
|
"$ref": "definitions/errors/error.yaml"
|
||||||
|
404:
|
||||||
|
description: |-
|
||||||
|
The backup was not found.
|
||||||
|
examples:
|
||||||
|
application/json: {
|
||||||
|
"errcode": "M_NOT_FOUND",
|
||||||
|
"error": "Unknown backup version"
|
||||||
|
}
|
||||||
|
schema:
|
||||||
|
"$ref": "definitions/errors/error.yaml"
|
||||||
|
429:
|
||||||
|
description: This request was rate-limited.
|
||||||
|
schema:
|
||||||
|
"$ref": "definitions/errors/rate_limited.yaml"
|
||||||
|
tags:
|
||||||
|
- End-to-end encryption
|
||||||
|
get:
|
||||||
|
summary: Retrieve the keys from the backup for a given room
|
||||||
|
description: |-
|
||||||
|
Retrieve the keys from the backup for a given room
|
||||||
|
operationId: getRoomKeysKeyRoomId
|
||||||
|
security:
|
||||||
|
- accessToken: []
|
||||||
|
parameters:
|
||||||
|
- in: query
|
||||||
|
type: string
|
||||||
|
name: version
|
||||||
|
description: |-
|
||||||
|
The backup from which to retrieve the key
|
||||||
|
required: true
|
||||||
|
x-example: "1"
|
||||||
|
- in: path
|
||||||
|
type: string
|
||||||
|
name: roomId
|
||||||
|
description: The ID of the room that the requested key is for.
|
||||||
|
required: true
|
||||||
|
x-example: "!roomid:example.org"
|
||||||
|
responses:
|
||||||
|
200:
|
||||||
|
description: |-
|
||||||
|
The key data. If no keys are found, then an object with an empty
|
||||||
|
``sessions`` property will be returned (``{"sessions": {}}``).
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
sessions:
|
||||||
|
type: object
|
||||||
|
description: |-
|
||||||
|
A map of session IDs to key data.
|
||||||
|
additionalProperties:
|
||||||
|
allOf:
|
||||||
|
- $ref: "definitions/key_backup_data.yaml"
|
||||||
|
example: {
|
||||||
|
"sessionid1": {
|
||||||
|
"ephemeral": "base64+ephemeral+key",
|
||||||
|
"ciphertext": "base64+ciphertext+of+JSON+data",
|
||||||
|
"mac": "base64+mac+of+ciphertext"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
404:
|
||||||
|
description: |-
|
||||||
|
The backup was not found.
|
||||||
|
examples:
|
||||||
|
application/json: {
|
||||||
|
"errcode": "M_NOT_FOUND",
|
||||||
|
"error": "Unknown backup version"
|
||||||
|
}
|
||||||
|
schema:
|
||||||
|
"$ref": "definitions/errors/error.yaml"
|
||||||
|
429:
|
||||||
|
description: This request was rate-limited.
|
||||||
|
schema:
|
||||||
|
"$ref": "definitions/errors/rate_limited.yaml"
|
||||||
|
delete:
|
||||||
|
summary: Delete a key from the backup
|
||||||
|
description: |-
|
||||||
|
Delete a key from the backup.
|
||||||
|
operationId: deleteRoomKeysKeyRoomId
|
||||||
|
security:
|
||||||
|
- accessToken: []
|
||||||
|
parameters:
|
||||||
|
- in: query
|
||||||
|
type: string
|
||||||
|
name: version
|
||||||
|
description: |-
|
||||||
|
The backup from which to delete the key
|
||||||
|
required: true
|
||||||
|
x-example: "1"
|
||||||
|
- in: path
|
||||||
|
type: string
|
||||||
|
name: roomId
|
||||||
|
description: The ID of the room that the specified key is for.
|
||||||
|
required: true
|
||||||
|
x-example: "!roomid:example.org"
|
||||||
|
responses:
|
||||||
|
200:
|
||||||
|
description: The update succeeded
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
etag:
|
||||||
|
description: |-
|
||||||
|
The new etag value representing stored keys in the backup.
|
||||||
|
See ``GET /room_keys/version/{version}`` for more details.
|
||||||
|
type: string
|
||||||
|
example: "abcdefg"
|
||||||
|
count:
|
||||||
|
description: The number of keys stored in the backup
|
||||||
|
type: integer
|
||||||
|
example: 10
|
||||||
|
required:
|
||||||
|
- etag
|
||||||
|
- count
|
||||||
|
404:
|
||||||
|
description: |-
|
||||||
|
The backup was not found.
|
||||||
|
examples:
|
||||||
|
application/json: {
|
||||||
|
"errcode": "M_NOT_FOUND",
|
||||||
|
"error": "Unknown backup version"
|
||||||
|
}
|
||||||
|
schema:
|
||||||
|
"$ref": "definitions/errors/error.yaml"
|
||||||
|
429:
|
||||||
|
description: This request was rate-limited.
|
||||||
|
schema:
|
||||||
|
"$ref": "definitions/errors/rate_limited.yaml"
|
||||||
|
"/room_keys/keys":
|
||||||
|
put:
|
||||||
|
summary: Store several keys in the backup.
|
||||||
|
description: |-
|
||||||
|
Store several keys in the backup.
|
||||||
|
operationId: postRoomKeysKey
|
||||||
|
security:
|
||||||
|
- accessToken: []
|
||||||
|
parameters:
|
||||||
|
- in: query
|
||||||
|
type: string
|
||||||
|
name: version
|
||||||
|
description: |-
|
||||||
|
The backup in which to store the keys. Must be the current backup.
|
||||||
|
required: true
|
||||||
|
x-example: "1"
|
||||||
|
- in: body
|
||||||
|
description: "The backup data"
|
||||||
|
name: backupData
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
rooms:
|
||||||
|
type: object
|
||||||
|
description: |-
|
||||||
|
A map of room IDs to session IDs to key data.
|
||||||
|
additionalProperties:
|
||||||
|
type: object
|
||||||
|
additionalProperties:
|
||||||
|
allOf:
|
||||||
|
- $ref: "definitions/key_backup_data.yaml"
|
||||||
|
example: {
|
||||||
|
"!room:example.org": {
|
||||||
|
"sessions": {
|
||||||
|
"sessionid1": {
|
||||||
|
"ephemeral": "base64+ephemeral+key",
|
||||||
|
"ciphertext": "base64+ciphertext+of+JSON+data",
|
||||||
|
"mac": "base64+mac+of+ciphertext"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
responses:
|
||||||
|
200:
|
||||||
|
description: The update succeeded
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
etag:
|
||||||
|
description: |-
|
||||||
|
The new etag value representing stored keys in the backup.
|
||||||
|
See ``GET /room_keys/version/{version}`` for more details.
|
||||||
|
type: string
|
||||||
|
example: "abcdefg"
|
||||||
|
count:
|
||||||
|
description: The number of keys stored in the backup
|
||||||
|
type: integer
|
||||||
|
example: 10
|
||||||
|
required:
|
||||||
|
- etag
|
||||||
|
- count
|
||||||
|
403:
|
||||||
|
description: |-
|
||||||
|
The version specified does not match the current backup version.
|
||||||
|
The current version will be included in the ``current_version``
|
||||||
|
field.
|
||||||
|
examples:
|
||||||
|
application/json: {
|
||||||
|
"errcode": "M_WRONG_ROOM_KEYS_VERSION",
|
||||||
|
"error": "Wrong backup version.",
|
||||||
|
"current_version": "42"
|
||||||
|
}
|
||||||
|
schema:
|
||||||
|
"$ref": "definitions/errors/error.yaml"
|
||||||
|
404:
|
||||||
|
description: |-
|
||||||
|
The backup was not found.
|
||||||
|
examples:
|
||||||
|
application/json: {
|
||||||
|
"errcode": "M_NOT_FOUND",
|
||||||
|
"error": "Unknown backup version"
|
||||||
|
}
|
||||||
|
schema:
|
||||||
|
"$ref": "definitions/errors/error.yaml"
|
||||||
|
429:
|
||||||
|
description: This request was rate-limited.
|
||||||
|
schema:
|
||||||
|
"$ref": "definitions/errors/rate_limited.yaml"
|
||||||
|
tags:
|
||||||
|
- End-to-end encryption
|
||||||
|
get:
|
||||||
|
summary: Retrieve the keys from the backup for a given room
|
||||||
|
description: |-
|
||||||
|
Retrieve the keys from the backup for a given room
|
||||||
|
operationId: getRoomKeysKeyRoomId
|
||||||
|
security:
|
||||||
|
- accessToken: []
|
||||||
|
parameters:
|
||||||
|
- in: query
|
||||||
|
type: string
|
||||||
|
name: version
|
||||||
|
description: |-
|
||||||
|
The backup from which to retrieve the keys. If omitted, the keys are
|
||||||
|
retrieved from the current backup.
|
||||||
|
x-example: "1"
|
||||||
|
responses:
|
||||||
|
200:
|
||||||
|
description: |-
|
||||||
|
The key data. If no keys are found, then an object with an empty
|
||||||
|
``rooms`` property will be returned (``{"rooms": {}}``).
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
rooms:
|
||||||
|
type: object
|
||||||
|
description: |-
|
||||||
|
A map of room IDs to session IDs to key data.
|
||||||
|
additionalProperties:
|
||||||
|
type: object
|
||||||
|
additionalProperties:
|
||||||
|
allOf:
|
||||||
|
- $ref: "definitions/key_backup_data.yaml"
|
||||||
|
example: {
|
||||||
|
"!room:example.org": {
|
||||||
|
"sessions": {
|
||||||
|
"sessionid1": {
|
||||||
|
"ephemeral": "base64+ephemeral+key",
|
||||||
|
"ciphertext": "base64+ciphertext+of+JSON+data",
|
||||||
|
"mac": "base64+mac+of+ciphertext"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
404:
|
||||||
|
description: The backup was not found.
|
||||||
|
examples:
|
||||||
|
application/json: {
|
||||||
|
"errcode": "M_NOT_FOUND",
|
||||||
|
"error": "Unknown backup version."
|
||||||
|
}
|
||||||
|
schema:
|
||||||
|
"$ref": "definitions/errors/error.yaml"
|
||||||
|
429:
|
||||||
|
description: This request was rate-limited.
|
||||||
|
schema:
|
||||||
|
"$ref": "definitions/errors/rate_limited.yaml"
|
||||||
|
delete:
|
||||||
|
summary: Delete a key from the backup
|
||||||
|
description: |-
|
||||||
|
Delete a key from the backup.
|
||||||
|
operationId: deleteRoomKeysKeyRoomId
|
||||||
|
security:
|
||||||
|
- accessToken: []
|
||||||
|
parameters:
|
||||||
|
- in: query
|
||||||
|
type: string
|
||||||
|
name: version
|
||||||
|
description: |-
|
||||||
|
The backup from which to delete the key
|
||||||
|
required: true
|
||||||
|
x-example: "1"
|
||||||
|
responses:
|
||||||
|
200:
|
||||||
|
description: The update succeeded
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
etag:
|
||||||
|
description: |-
|
||||||
|
The new etag value representing stored keys in the backup.
|
||||||
|
See ``GET /room_keys/version/{version}`` for more details.
|
||||||
|
type: string
|
||||||
|
example: "abcdefg"
|
||||||
|
count:
|
||||||
|
description: The number of keys stored in the backup
|
||||||
|
type: integer
|
||||||
|
example: 10
|
||||||
|
required:
|
||||||
|
- etag
|
||||||
|
- count
|
||||||
|
404:
|
||||||
|
description: |-
|
||||||
|
The backup was not found.
|
||||||
|
examples:
|
||||||
|
application/json: {
|
||||||
|
"errcode": "M_NOT_FOUND",
|
||||||
|
"error": "Unknown backup version"
|
||||||
|
}
|
||||||
|
schema:
|
||||||
|
"$ref": "definitions/errors/error.yaml"
|
||||||
|
429:
|
||||||
|
description: This request was rate-limited.
|
||||||
|
schema:
|
||||||
|
"$ref": "definitions/errors/rate_limited.yaml"
|
1
changelogs/client_server/2387.new
Normal file
1
changelogs/client_server/2387.new
Normal file
|
@ -0,0 +1 @@
|
||||||
|
Add key backup (``/room_keys/*``) endpoints.
|
|
@ -781,6 +781,136 @@ previously-received ``request`` message with the same ``request_id`` and
|
||||||
A reasonable strategy is for a user's client to only send keys requested by the
|
A reasonable strategy is for a user's client to only send keys requested by the
|
||||||
verified devices of the same user.
|
verified devices of the same user.
|
||||||
|
|
||||||
|
Server-side key backups
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
Devices may upload encrypted copies of keys to the server. When a device tries
|
||||||
|
to read a message that it does not have keys for, it may request the key from
|
||||||
|
the server and decrypt it. Backups are per-user, and users may replace backups
|
||||||
|
with new backups.
|
||||||
|
|
||||||
|
In contrast with `Key requests`_, Server-side key backups do not require another
|
||||||
|
device to be online from which to request keys. However, as the session keys are
|
||||||
|
stored on the server encrypted, it requires users to enter a decryption key to
|
||||||
|
decrypt the session keys.
|
||||||
|
|
||||||
|
To create a backup, a client will call `POST
|
||||||
|
/_matrix/client/r0/room_keys/version`_ and define how the keys are to be
|
||||||
|
encrypted through the backup's ``auth_data``; other clients can discover the
|
||||||
|
backup by calling `GET /_matrix/client/r0/room_keys/version`_. Keys are
|
||||||
|
encrypted according to the backup's ``auth_data`` and added to the backup by
|
||||||
|
calling `PUT /_matrix/client/r0/room_keys/keys`_ or one of its variants, and
|
||||||
|
can be retrieved by calling `GET /_matrix/client/r0/room_keys/keys`_ or one of
|
||||||
|
its variants. Keys can only be written to the most recently created version of
|
||||||
|
the backup. Backups can also be deleted using `DELETE
|
||||||
|
/_matrix/client/r0/room_keys/version/{version}`_, or individual keys can be
|
||||||
|
deleted using `DELETE /_matrix/client/r0/room_keys/keys`_ or one of its
|
||||||
|
variants.
|
||||||
|
|
||||||
|
Clients must only store keys in backups after they have ensured that the
|
||||||
|
``auth_data`` is trusted, either by checking the signatures on it, or by
|
||||||
|
deriving the public key from a private key that it obtained from a trusted
|
||||||
|
source.
|
||||||
|
|
||||||
|
When a client uploads a key for a session that the server already has a key
|
||||||
|
for, the server will choose to either keep the existing key or replace it with
|
||||||
|
the new key based on the key metadata as follows:
|
||||||
|
|
||||||
|
- if the keys have different values for ``is_verified``, then it will keep the
|
||||||
|
key that has ``is_verified`` set to ``true``;
|
||||||
|
- if they have the same values for ``is_verified``, then it will keep the key
|
||||||
|
with a lower ``first_message_index``;
|
||||||
|
- and finally, is ``is_verified`` and ``first_message_index`` are equal, then
|
||||||
|
it will keep the key with a lower ``forwarded_count``.
|
||||||
|
|
||||||
|
Recovery key
|
||||||
|
<<<<<<<<<<<<
|
||||||
|
|
||||||
|
If the recovery key (the private half of the backup encryption key) is
|
||||||
|
presented to the user to save, it is presented as a string constructed as
|
||||||
|
follows:
|
||||||
|
|
||||||
|
1. The 256-bit curve25519 private key is prepended by the bytes ``0x8B`` and
|
||||||
|
``0x01``
|
||||||
|
2. All the bytes in the string above, including the two header bytes, are XORed
|
||||||
|
together to form a parity byte. This parity byte is appended to the byte
|
||||||
|
string.
|
||||||
|
3. The byte string is encoded using base58, using the same `mapping as is used
|
||||||
|
for Bitcoin addresses
|
||||||
|
<https://en.bitcoin.it/wiki/Base58Check_encoding#Base58_symbol_chart>`_,
|
||||||
|
that is, using the alphabet
|
||||||
|
``123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz``.
|
||||||
|
4. A space should be added after every 4th character.
|
||||||
|
|
||||||
|
When reading in a recovery key, clients must disregard whitespace, and perform
|
||||||
|
the reverse of steps 1 through 3.
|
||||||
|
|
||||||
|
Backup algorithm: ``m.megolm_backup.v1.curve25519-aes-sha2``
|
||||||
|
<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
|
||||||
|
|
||||||
|
When a backup is created with the ``algorithm`` set to
|
||||||
|
``m.megolm_backup.v1.curve25519-aes-sha2``, the ``auth_data`` should have the
|
||||||
|
following format:
|
||||||
|
|
||||||
|
``AuthData``
|
||||||
|
|
||||||
|
.. table::
|
||||||
|
:widths: auto
|
||||||
|
|
||||||
|
========== =========== ======================================================
|
||||||
|
Parameter Type Description
|
||||||
|
========== =========== ======================================================
|
||||||
|
public_key string **Required.** The curve25519 public key used to encrypt
|
||||||
|
the backups, encoded in unpadded base64.
|
||||||
|
signatures Signatures Optional. Signatures of the ``auth_data``, as Signed
|
||||||
|
JSON
|
||||||
|
========== =========== ======================================================
|
||||||
|
|
||||||
|
The ``session_data`` field in the backups is constructed as follows:
|
||||||
|
|
||||||
|
1. Encode the session key to be backed up as a JSON object with the properties:
|
||||||
|
|
||||||
|
.. table::
|
||||||
|
:widths: auto
|
||||||
|
|
||||||
|
=============================== ======== =========================================
|
||||||
|
Parameter Type Description
|
||||||
|
=============================== ======== =========================================
|
||||||
|
algorithm string **Required.** The end-to-end message
|
||||||
|
encryption algorithm that the key is
|
||||||
|
for. Must be ``m.megolm.v1.aes-sha2``.
|
||||||
|
forwarding_curve25519_key_chain [string] **Required.** Chain of Curve25519 keys
|
||||||
|
through which this session was
|
||||||
|
forwarded, via
|
||||||
|
`m.forwarded_room_key`_ events.
|
||||||
|
sender_key string **Required.** Unpadded base64-encoded
|
||||||
|
device curve25519 key.
|
||||||
|
sender_claimed_keys {string: **Required.** A map from algorithm name
|
||||||
|
string} (``ed25519``) to the identity key
|
||||||
|
for the sending device.
|
||||||
|
session_key string **Required.** Unpadded base64-encoded
|
||||||
|
session key in `session-sharing format
|
||||||
|
<https://gitlab.matrix.org/matrix-org/olm/blob/master/docs/megolm.md#session-sharing-format>`_.
|
||||||
|
=============================== ======== =========================================
|
||||||
|
|
||||||
|
2. Generate an ephemeral curve25519 key, and perform an ECDH with the ephemeral
|
||||||
|
key and the backup's public key to generate a shared secret. The public
|
||||||
|
half of the ephemeral key, encoded using unpadded base64, becomes the ``ephemeral``
|
||||||
|
property of the ``session_data``.
|
||||||
|
3. Using the shared secret, generate 80 bytes by performing an HKDF using
|
||||||
|
SHA-256 as the hash, with a salt of 32 bytes of 0, and with the empty string
|
||||||
|
as the info. The first 32 bytes are used as the AES key, the next 32 bytes
|
||||||
|
are used as the MAC key, and the last 16 bytes are used as the AES
|
||||||
|
initialization vector.
|
||||||
|
4. Stringify the JSON object, and encrypt it using AES-CBC-256 with PKCS#7
|
||||||
|
padding. This encrypted data, encoded using unpadded base64, becomes the
|
||||||
|
``ciphertext`` property of the ``session_data``.
|
||||||
|
5. Pass the raw encrypted data (prior to base64 encoding) through HMAC-SHA-256
|
||||||
|
using the MAC key generated above. The first 8 bytes of the resulting MAC
|
||||||
|
are base64-encoded, and become the ``mac`` property of the ``session_data``.
|
||||||
|
|
||||||
|
{{key_backup_cs_http_api}}
|
||||||
|
|
||||||
Key exports
|
Key exports
|
||||||
~~~~~~~~~~~
|
~~~~~~~~~~~
|
||||||
|
|
||||||
|
@ -854,6 +984,9 @@ described as follows:
|
||||||
session_key string Required. The key for the session.
|
session_key string Required. The key for the session.
|
||||||
=============================== =========== ====================================
|
=============================== =========== ====================================
|
||||||
|
|
||||||
|
This is similar to the format before encryption used for the session keys in
|
||||||
|
`Server-side key backups`_ but adds the ``room_id`` and ``session_id`` fields.
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
|
|
||||||
.. code:: json
|
.. code:: json
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue