@dashevo/app-connect-contract 4.2.0-beta.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/.mocharc.yml ADDED
@@ -0,0 +1,2 @@
1
+ require: test/bootstrap.js
2
+ recursive: true
package/Cargo.toml ADDED
@@ -0,0 +1,15 @@
1
+ [package]
2
+ name = "app-connect-contract"
3
+ description = "App Connect data contract schema and tools. The wallet's encrypted response to an app login request"
4
+ version.workspace = true
5
+ edition = "2021"
6
+ rust-version.workspace = true
7
+ license = "MIT"
8
+
9
+ [dependencies]
10
+ thiserror = "2.0.12"
11
+ platform-version = { path = "../rs-platform-version" }
12
+ serde_json = { version = "1.0" }
13
+ platform-value = { path = "../rs-platform-value" }
14
+ [dev-dependencies]
15
+ base58 = "0.2.0"
package/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2019 Dash Core Group, Inc.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
6
+ this software and associated documentation files (the "Software"), to deal in
7
+ the Software without restriction, including without limitation the rights to
8
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
9
+ the Software, and to permit persons to whom the Software is furnished to do so,
10
+ subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
17
+ FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
18
+ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
19
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
20
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,39 @@
1
+ # App Connect Contract
2
+
3
+ The App Connect system contract carries the wallet's encrypted response to an
4
+ app login request. It activates at protocol version 14 and has the same ID on
5
+ every network: `H8F9mP1BM55TE1ShsxPZHzhyinaMdY9bMmP85mkDhcJJ`.
6
+
7
+ Its only document type is `loginKeyResponse`, written by the wallet's identity
8
+ after the user approves:
9
+
10
+ | Property | Bytes | Meaning |
11
+ | --- | --- | --- |
12
+ | `appEphemeralPubKeyHash` | 20 | `hash160` of the ephemeral public key in the app's request |
13
+ | `walletEphemeralPubKey` | 33 | Compressed ephemeral public key used to derive the shared secret |
14
+ | `encryptedPayload` | 60–572 | Encrypted granted private keys: 28-byte envelope plus 32 bytes per key, for 1–17 keys (session key plus up to eight bindings with both purposes) |
15
+
16
+ All three properties are required. The contract checks their byte lengths;
17
+ clients validate the public key, ciphertext framing and decrypted grant.
18
+
19
+ The type is `indexOnly`, immutable and deletable. Its `byRequest` index is a
20
+ flat composite terminal `(appEphemeralPubKeyHash, $ownerId)`. The wallet key and
21
+ ciphertext are `entryPayload` values, so there is no primary document row or
22
+ per-request tree. Each identity may answer a request once; another identity's
23
+ answer cannot reserve the wallet's entry. Apps query by request hash and
24
+ validate each candidate's ciphertext and granted keys against its `$ownerId`.
25
+
26
+ On re-login the wallet deletes its old response **by values** and creates the
27
+ new response. It retains the previous request hash, wallet key and ciphertext
28
+ for deletion; retaining only a document ID is insufficient for an index-only
29
+ type. See [the protocol guide](../../docs/protocol/app-connect.md) for details.
30
+
31
+ ## Install
32
+
33
+ ```sh
34
+ npm install @dashevo/app-connect-contract
35
+ ```
36
+
37
+ ## License
38
+
39
+ [MIT](LICENSE) © Dash Core Group, Inc.
@@ -0,0 +1,10 @@
1
+ import baseConfig from '../../eslint/base.mjs';
2
+ import mochaTestConfig from '../../eslint/mocha-tests.mjs';
3
+
4
+ export default [
5
+ ...baseConfig,
6
+ mochaTestConfig,
7
+ {
8
+ ignores: ['dist/**', 'node_modules/**'],
9
+ },
10
+ ];
@@ -0,0 +1,4 @@
1
+ module.exports = {
2
+ ownerId: '11111111111111111111111111111111',
3
+ contractId: 'H8F9mP1BM55TE1ShsxPZHzhyinaMdY9bMmP85mkDhcJJ',
4
+ };
package/package.json ADDED
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "@dashevo/app-connect-contract",
3
+ "version": "4.2.0-beta.4",
4
+ "description": "A system contract for encrypted wallet-to-app login responses",
5
+ "scripts": {
6
+ "lint": "eslint .",
7
+ "test": "yarn run test:unit",
8
+ "test:unit": "mocha 'test/unit/**/*.spec.js'"
9
+ },
10
+ "contributors": [
11
+ {
12
+ "name": "Pasta",
13
+ "email": "pasta@dashboost.org",
14
+ "url": "https://github.com/PastaPastaPasta"
15
+ }
16
+ ],
17
+ "license": "MIT",
18
+ "devDependencies": {
19
+ "@dashevo/wasm-dpp": "workspace:*",
20
+ "chai": "^4.3.10",
21
+ "dirty-chai": "^2.0.1",
22
+ "eslint": "^9.18.0",
23
+ "mocha": "^11.1.0",
24
+ "sinon": "^18.0.1",
25
+ "sinon-chai": "^3.7.0"
26
+ }
27
+ }
@@ -0,0 +1,55 @@
1
+ {
2
+ "loginKeyResponse": {
3
+ "type": "object",
4
+ "documentsMutable": false,
5
+ "canBeDeleted": true,
6
+ "creationRestrictionMode": 0,
7
+ "indices": [
8
+ {
9
+ "name": "byRequest",
10
+ "terminal": [
11
+ "appEphemeralPubKeyHash",
12
+ "$ownerId"
13
+ ]
14
+ }
15
+ ],
16
+ "properties": {
17
+ "appEphemeralPubKeyHash": {
18
+ "type": "array",
19
+ "byteArray": true,
20
+ "minItems": 20,
21
+ "maxItems": 20,
22
+ "description": "hash160 of the ephemeral public key the app put in its connect request; identifies the request",
23
+ "position": 0
24
+ },
25
+ "walletEphemeralPubKey": {
26
+ "type": "array",
27
+ "byteArray": true,
28
+ "minItems": 33,
29
+ "maxItems": 33,
30
+ "description": "The wallet's compressed ephemeral public key; the app derives the shared secret from it and its own ephemeral private key",
31
+ "position": 1
32
+ },
33
+ "encryptedPayload": {
34
+ "type": "array",
35
+ "byteArray": true,
36
+ "minItems": 60,
37
+ "maxItems": 572,
38
+ "description": "The granted private keys encrypted to the app: a 28-byte envelope plus 32 bytes per key, one to seventeen keys (the session key plus up to eight bindings with both purposes)",
39
+ "position": 2
40
+ }
41
+ },
42
+ "required": [
43
+ "appEphemeralPubKeyHash",
44
+ "walletEphemeralPubKey",
45
+ "encryptedPayload"
46
+ ],
47
+ "description": "A wallet's response after approving a login request. The flat entry is keyed by the request hash and wallet identity, so other identities cannot block its write. On re-login the wallet deletes its previous response by values and creates a response for the new request.",
48
+ "additionalProperties": false,
49
+ "indexOnly": true,
50
+ "entryPayload": [
51
+ "walletEphemeralPubKey",
52
+ "encryptedPayload"
53
+ ]
54
+ }
55
+ }
package/src/error.rs ADDED
@@ -0,0 +1,17 @@
1
+ use platform_version::version::FeatureVersion;
2
+
3
+ #[derive(thiserror::Error, Debug)]
4
+ pub enum Error {
5
+ /// Platform expected some specific versions
6
+ #[error("platform unknown version on {method}, received: {received}")]
7
+ UnknownVersionMismatch {
8
+ /// method
9
+ method: String,
10
+ /// the allowed versions for this method
11
+ known_versions: Vec<FeatureVersion>,
12
+ /// requested core height
13
+ received: FeatureVersion,
14
+ },
15
+ #[error("schema deserialize error: {0}")]
16
+ InvalidSchemaJson(#[from] serde_json::Error),
17
+ }
package/src/lib.rs ADDED
@@ -0,0 +1,66 @@
1
+ mod error;
2
+ pub mod v1;
3
+
4
+ pub use crate::error::Error;
5
+ use platform_value::{Identifier, IdentifierBytes32};
6
+ use platform_version::version::PlatformVersion;
7
+ use serde_json::Value;
8
+
9
+ pub const ID_BYTES: [u8; 32] = [
10
+ 239, 150, 14, 165, 105, 114, 235, 173, 190, 248, 162, 126, 247, 218, 92, 129, 255, 75, 179,
11
+ 138, 2, 150, 151, 69, 126, 36, 218, 66, 183, 155, 84, 183,
12
+ ];
13
+
14
+ pub const OWNER_ID_BYTES: [u8; 32] = [0; 32];
15
+
16
+ pub const ID: Identifier = Identifier(IdentifierBytes32(ID_BYTES));
17
+ pub const OWNER_ID: Identifier = Identifier(IdentifierBytes32(OWNER_ID_BYTES));
18
+ /// The contract's definitions for `platform_version`. Version 0, the value the tables of
19
+ /// protocol versions below 14 carry, names no schema generation and is refused like any
20
+ /// unknown version: the contract does not exist before its activation.
21
+ pub fn load_definitions(platform_version: &PlatformVersion) -> Result<Option<Value>, Error> {
22
+ match platform_version.system_data_contracts.app_connect {
23
+ 1 => Ok(None),
24
+ version => Err(Error::UnknownVersionMismatch {
25
+ method: "app_connect_contract::load_definitions".to_string(),
26
+ known_versions: vec![1],
27
+ received: version,
28
+ }),
29
+ }
30
+ }
31
+ pub fn load_documents_schemas(platform_version: &PlatformVersion) -> Result<Value, Error> {
32
+ match platform_version.system_data_contracts.app_connect {
33
+ 1 => v1::load_documents_schemas(),
34
+ version => Err(Error::UnknownVersionMismatch {
35
+ method: "app_connect_contract::load_documents_schemas".to_string(),
36
+ known_versions: vec![1],
37
+ received: version,
38
+ }),
39
+ }
40
+ }
41
+
42
+ #[cfg(test)]
43
+ mod tests {
44
+ use base58::FromBase58;
45
+
46
+ use super::*;
47
+
48
+ #[test]
49
+ /// Ensure that the ID constant matches the expected value
50
+ /// and that it can be encoded to base58 correctly.
51
+ fn should_match_the_published_system_contract_id() {
52
+ assert_eq!(
53
+ ID,
54
+ Identifier(IdentifierBytes32(ID_BYTES)),
55
+ "ID should match the expected value"
56
+ );
57
+
58
+ let base58_decoded = "H8F9mP1BM55TE1ShsxPZHzhyinaMdY9bMmP85mkDhcJJ"
59
+ .from_base58()
60
+ .unwrap();
61
+ assert_eq!(
62
+ base58_decoded, ID_BYTES,
63
+ "ID should match the base58 decoded value"
64
+ );
65
+ }
66
+ }
package/src/v1/mod.rs ADDED
@@ -0,0 +1,25 @@
1
+ use crate::Error;
2
+ use serde_json::Value;
3
+
4
+ pub mod document_types {
5
+ pub mod login_key_response {
6
+ pub const NAME: &str = "loginKeyResponse";
7
+
8
+ pub mod properties {
9
+ pub const APP_EPHEMERAL_PUB_KEY_HASH: &str = "appEphemeralPubKeyHash";
10
+ pub const WALLET_EPHEMERAL_PUB_KEY: &str = "walletEphemeralPubKey";
11
+ pub const ENCRYPTED_PAYLOAD: &str = "encryptedPayload";
12
+ }
13
+
14
+ pub mod indexes {
15
+ pub const BY_REQUEST: &str = "byRequest";
16
+ }
17
+ }
18
+ }
19
+
20
+ pub fn load_documents_schemas() -> Result<Value, Error> {
21
+ serde_json::from_str(include_str!(
22
+ "../../schema/v1/app-connect-contract-documents.json"
23
+ ))
24
+ .map_err(Error::InvalidSchemaJson)
25
+ }
@@ -0,0 +1,30 @@
1
+ const sinon = require('sinon');
2
+ const sinonChai = require('sinon-chai');
3
+
4
+ const { expect, use } = require('chai');
5
+ const dirtyChai = require('dirty-chai');
6
+
7
+ const {
8
+ default: loadWasmDpp,
9
+ } = require('@dashevo/wasm-dpp');
10
+
11
+ use(dirtyChai);
12
+ use(sinonChai);
13
+
14
+ exports.mochaHooks = {
15
+ beforeAll: loadWasmDpp,
16
+
17
+ beforeEach() {
18
+ if (!this.sinon) {
19
+ this.sinon = sinon.createSandbox();
20
+ } else {
21
+ this.sinon.restore();
22
+ }
23
+ },
24
+
25
+ afterEach() {
26
+ this.sinon.restore();
27
+ },
28
+ };
29
+
30
+ global.expect = expect;
@@ -0,0 +1,199 @@
1
+ const crypto = require('crypto');
2
+
3
+ const {
4
+ DashPlatformProtocol,
5
+ JsonSchemaError,
6
+ } = require('@dashevo/wasm-dpp');
7
+ const generateRandomIdentifier = require('@dashevo/wasm-dpp/lib/test/utils/generateRandomIdentifierAsync');
8
+
9
+ const { expect } = require('chai');
10
+ const appConnectContractDocumentsSchema = require('../../schema/v1/app-connect-contract-documents.json');
11
+
12
+ const expectJsonSchemaError = (validationResult, errorCount = 1) => {
13
+ const errors = validationResult.getErrors();
14
+ expect(errors)
15
+ .to
16
+ .have
17
+ .length(errorCount);
18
+
19
+ const error = validationResult.getErrors()[0];
20
+ expect(error)
21
+ .to
22
+ .be
23
+ .instanceof(JsonSchemaError);
24
+
25
+ return error;
26
+ };
27
+
28
+ describe('App Connect Contract', () => {
29
+ let dpp;
30
+ let dataContract;
31
+ let identityId;
32
+
33
+ beforeEach(async () => {
34
+ dpp = new DashPlatformProtocol(
35
+ { generate: () => crypto.randomBytes(32) },
36
+ );
37
+
38
+ identityId = await generateRandomIdentifier();
39
+
40
+ dataContract = dpp.dataContract.create(
41
+ identityId,
42
+ BigInt(1),
43
+ appConnectContractDocumentsSchema,
44
+ );
45
+ });
46
+
47
+ it('should have a valid contract definition', async () => {
48
+ expect(() => dpp.dataContract.create(
49
+ identityId,
50
+ BigInt(1),
51
+ appConnectContractDocumentsSchema,
52
+ ))
53
+ .to
54
+ .not
55
+ .throw();
56
+ });
57
+
58
+ describe('documents', () => {
59
+ describe('loginKeyResponse', () => {
60
+ let rawLoginKeyResponseDocument;
61
+
62
+ beforeEach(() => {
63
+ rawLoginKeyResponseDocument = {
64
+ appEphemeralPubKeyHash: crypto.randomBytes(20),
65
+ walletEphemeralPubKey: crypto.randomBytes(33),
66
+ encryptedPayload: crypto.randomBytes(60),
67
+ };
68
+ });
69
+
70
+ describe('appEphemeralPubKeyHash', () => {
71
+ it('should be defined', async () => {
72
+ delete rawLoginKeyResponseDocument.appEphemeralPubKeyHash;
73
+
74
+ const document = dpp.document.create(dataContract, identityId, 'loginKeyResponse', rawLoginKeyResponseDocument);
75
+ const validationResult = document.validate(dpp.protocolVersion);
76
+ const error = expectJsonSchemaError(validationResult);
77
+
78
+ expect(error.keyword).to.equal('required');
79
+ expect(error.params.missingProperty).to.equal('appEphemeralPubKeyHash');
80
+ });
81
+
82
+ it('should be not shorter than 20 bytes', async () => {
83
+ rawLoginKeyResponseDocument.appEphemeralPubKeyHash = crypto.randomBytes(19);
84
+
85
+ const document = dpp.document.create(dataContract, identityId, 'loginKeyResponse', rawLoginKeyResponseDocument);
86
+ const validationResult = document.validate(dpp.protocolVersion);
87
+ const error = expectJsonSchemaError(validationResult);
88
+
89
+ expect(error.keyword).to.equal('minItems');
90
+ });
91
+
92
+ it('should be not longer than 20 bytes', async () => {
93
+ rawLoginKeyResponseDocument.appEphemeralPubKeyHash = crypto.randomBytes(21);
94
+
95
+ const document = dpp.document.create(dataContract, identityId, 'loginKeyResponse', rawLoginKeyResponseDocument);
96
+ const validationResult = document.validate(dpp.protocolVersion);
97
+ const error = expectJsonSchemaError(validationResult);
98
+
99
+ expect(error.keyword).to.equal('maxItems');
100
+ });
101
+ });
102
+
103
+ describe('walletEphemeralPubKey', () => {
104
+ it('should be defined', async () => {
105
+ delete rawLoginKeyResponseDocument.walletEphemeralPubKey;
106
+
107
+ const document = dpp.document.create(dataContract, identityId, 'loginKeyResponse', rawLoginKeyResponseDocument);
108
+ const validationResult = document.validate(dpp.protocolVersion);
109
+ const error = expectJsonSchemaError(validationResult);
110
+
111
+ expect(error.keyword).to.equal('required');
112
+ expect(error.params.missingProperty).to.equal('walletEphemeralPubKey');
113
+ });
114
+
115
+ it('should be not shorter than 33 bytes', async () => {
116
+ rawLoginKeyResponseDocument.walletEphemeralPubKey = crypto.randomBytes(32);
117
+
118
+ const document = dpp.document.create(dataContract, identityId, 'loginKeyResponse', rawLoginKeyResponseDocument);
119
+ const validationResult = document.validate(dpp.protocolVersion);
120
+ const error = expectJsonSchemaError(validationResult);
121
+
122
+ expect(error.keyword).to.equal('minItems');
123
+ });
124
+
125
+ it('should be not longer than 33 bytes', async () => {
126
+ rawLoginKeyResponseDocument.walletEphemeralPubKey = crypto.randomBytes(34);
127
+
128
+ const document = dpp.document.create(dataContract, identityId, 'loginKeyResponse', rawLoginKeyResponseDocument);
129
+ const validationResult = document.validate(dpp.protocolVersion);
130
+ const error = expectJsonSchemaError(validationResult);
131
+
132
+ expect(error.keyword).to.equal('maxItems');
133
+ });
134
+ });
135
+
136
+ describe('encryptedPayload', () => {
137
+ it('should be defined', async () => {
138
+ delete rawLoginKeyResponseDocument.encryptedPayload;
139
+
140
+ const document = dpp.document.create(dataContract, identityId, 'loginKeyResponse', rawLoginKeyResponseDocument);
141
+ const validationResult = document.validate(dpp.protocolVersion);
142
+ const error = expectJsonSchemaError(validationResult);
143
+
144
+ expect(error.keyword).to.equal('required');
145
+ expect(error.params.missingProperty).to.equal('encryptedPayload');
146
+ });
147
+
148
+ it('should be not shorter than 60 bytes', async () => {
149
+ rawLoginKeyResponseDocument.encryptedPayload = crypto.randomBytes(59);
150
+
151
+ const document = dpp.document.create(dataContract, identityId, 'loginKeyResponse', rawLoginKeyResponseDocument);
152
+ const validationResult = document.validate(dpp.protocolVersion);
153
+ const error = expectJsonSchemaError(validationResult);
154
+
155
+ expect(error.keyword).to.equal('minItems');
156
+ });
157
+
158
+ it('should be not longer than 572 bytes', async () => {
159
+ rawLoginKeyResponseDocument.encryptedPayload = crypto.randomBytes(573);
160
+
161
+ const document = dpp.document.create(dataContract, identityId, 'loginKeyResponse', rawLoginKeyResponseDocument);
162
+ const validationResult = document.validate(dpp.protocolVersion);
163
+ const error = expectJsonSchemaError(validationResult);
164
+
165
+ expect(error.keyword).to.equal('maxItems');
166
+ });
167
+
168
+ it('should accept the maximum grant of seventeen keys', async () => {
169
+ // Session key plus eight bindings asking for both
170
+ // purposes: 28-byte envelope + 17 * 32 = 572 bytes.
171
+ rawLoginKeyResponseDocument.encryptedPayload = crypto.randomBytes(28 + (17 * 32));
172
+
173
+ const document = dpp.document.create(dataContract, identityId, 'loginKeyResponse', rawLoginKeyResponseDocument);
174
+ const validationResult = document.validate(dpp.protocolVersion);
175
+
176
+ expect(validationResult.isValid()).to.be.true();
177
+ });
178
+ });
179
+
180
+ it('should not have additional properties', async () => {
181
+ rawLoginKeyResponseDocument.someOtherProperty = 42;
182
+
183
+ const document = dpp.document.create(dataContract, identityId, 'loginKeyResponse', rawLoginKeyResponseDocument);
184
+ const validationResult = document.validate(dpp.protocolVersion);
185
+ const error = expectJsonSchemaError(validationResult);
186
+
187
+ expect(error.keyword).to.equal('additionalProperties');
188
+ expect(error.params.additionalProperties).to.deep.equal(['someOtherProperty']);
189
+ });
190
+
191
+ it('should be valid', async () => {
192
+ const document = dpp.document.create(dataContract, identityId, 'loginKeyResponse', rawLoginKeyResponseDocument);
193
+ const validationResult = document.validate(dpp.protocolVersion);
194
+
195
+ expect(validationResult.isValid()).to.be.true();
196
+ });
197
+ });
198
+ });
199
+ });