diff --git a/services/identity/src/client_service.rs b/services/identity/src/client_service.rs --- a/services/identity/src/client_service.rs +++ b/services/identity/src/client_service.rs @@ -35,7 +35,7 @@ use crate::grpc_utils::{ DeviceKeyUploadActions, RegistrationActions, SignedNonce }; -use crate::nonce::generate_nonce_data; +use crate::nonce::{generate_nonce_data, NonceVerification}; use crate::reserved_users::{ validate_account_ownership_message_and_get_user_id, validate_add_reserved_usernames_message, @@ -92,7 +92,7 @@ #[derive(derive_more::Constructor)] pub struct ClientService { - client: DatabaseClient, + pub client: DatabaseClient, } #[tonic::async_trait] @@ -1064,37 +1064,6 @@ } } - async fn verify_and_remove_nonce( - &self, - nonce: &str, - ) -> Result<(), tonic::Status> { - match self - .client - .get_nonce_from_nonces_table(nonce) - .await - .map_err(handle_db_error)? - { - None => { - return Err(tonic::Status::invalid_argument( - tonic_status_messages::INVALID_NONCE, - )) - } - Some(nonce) if nonce.is_expired() => { - // we don't need to remove the nonce from the table here - // because the DynamoDB TTL will take care of it - return Err(tonic::Status::aborted( - tonic_status_messages::NONCE_EXPIRED, - )); - } - Some(nonce_data) => self - .client - .remove_nonce_from_nonces_table(&nonce_data.nonce) - .await - .map_err(handle_db_error)?, - }; - Ok(()) - } - async fn get_keyserver_device_to_remove( &self, user_id: &str, diff --git a/services/identity/src/grpc_services/authenticated.rs b/services/identity/src/grpc_services/authenticated.rs --- a/services/identity/src/grpc_services/authenticated.rs +++ b/services/identity/src/grpc_services/authenticated.rs @@ -16,7 +16,7 @@ use tracing::{debug, error, trace, warn}; use super::protos::auth::{ - identity_client_service_server::IdentityClientService, + identity_client_service_server::IdentityClientService, AddWalletRequest, DeletePasswordUserFinishRequest, DeletePasswordUserStartRequest, DeletePasswordUserStartResponse, GetDeviceListRequest, GetDeviceListResponse, InboundKeyInfo, InboundKeysForUserRequest, InboundKeysForUserResponse, @@ -32,7 +32,7 @@ #[derive(derive_more::Constructor)] pub struct AuthenticatedService { - db_client: DatabaseClient, + pub db_client: DatabaseClient, } fn get_auth_info(req: &Request<()>) -> Option<(String, String, String)> { @@ -824,6 +824,13 @@ Ok(Response::new(Empty {})) } + + async fn add_wallet( + &self, + _request: tonic::Request, + ) -> Result, tonic::Status> { + unimplemented!() + } } #[allow(dead_code)] diff --git a/services/identity/src/grpc_services/shared.rs b/services/identity/src/grpc_services/shared.rs --- a/services/identity/src/grpc_services/shared.rs +++ b/services/identity/src/grpc_services/shared.rs @@ -2,12 +2,16 @@ use tonic::{Request, Status}; use tracing::trace; +use crate::client_service::ClientService; use crate::constants::{ request_metadata, tonic_status_messages, MIN_SUPPORTED_NATIVE_VERSION, }; +use crate::database::DatabaseClient; pub use grpc_clients::identity::shared::PlatformMetadata; +use super::authenticated::AuthenticatedService; + pub fn version_interceptor(req: Request<()>) -> Result, Status> { trace!("Intercepting request to check version: {:?}", req); @@ -61,3 +65,19 @@ let raw_value = req.metadata().get(key)?; raw_value.to_str().ok().map(|s| s.to_string()) } + +pub trait HasClient { + fn client(&self) -> &DatabaseClient; +} + +impl HasClient for ClientService { + fn client(&self) -> &DatabaseClient { + &self.client + } +} + +impl HasClient for AuthenticatedService { + fn client(&self) -> &DatabaseClient { + &self.db_client + } +} diff --git a/services/identity/src/nonce.rs b/services/identity/src/nonce.rs --- a/services/identity/src/nonce.rs +++ b/services/identity/src/nonce.rs @@ -4,8 +4,11 @@ CryptoRng, Rng, }; -use crate::constants::NONCE_LENGTH; -use crate::constants::NONCE_TTL_DURATION; +use crate::{ + client_service::handle_db_error, + constants::{tonic_status_messages, NONCE_TTL_DURATION}, +}; +use crate::{constants::NONCE_LENGTH, grpc_services::shared::HasClient}; pub fn generate_nonce_data(rng: &mut (impl Rng + CryptoRng)) -> NonceData { let nonce = Alphanumeric.sample_string(rng, NONCE_LENGTH); @@ -30,3 +33,45 @@ Utc::now() > self.expiration_time } } + +#[tonic::async_trait] +pub trait NonceVerification { + async fn verify_and_remove_nonce( + &self, + nonce: &str, + ) -> Result<(), tonic::Status>; +} + +#[tonic::async_trait] +impl NonceVerification for T { + async fn verify_and_remove_nonce( + &self, + nonce: &str, + ) -> Result<(), tonic::Status> { + match self + .client() + .get_nonce_from_nonces_table(nonce) + .await + .map_err(handle_db_error)? + { + None => { + return Err(tonic::Status::invalid_argument( + tonic_status_messages::INVALID_NONCE, + )) + } + Some(nonce) if nonce.is_expired() => { + // we don't need to remove the nonce from the table here + // because the DynamoDB TTL will take care of it + return Err(tonic::Status::aborted( + tonic_status_messages::NONCE_EXPIRED, + )); + } + Some(nonce_data) => self + .client() + .remove_nonce_from_nonces_table(&nonce_data.nonce) + .await + .map_err(handle_db_error)?, + }; + Ok(()) + } +} diff --git a/shared/protos/identity_auth.proto b/shared/protos/identity_auth.proto --- a/shared/protos/identity_auth.proto +++ b/shared/protos/identity_auth.proto @@ -90,6 +90,8 @@ // It doesn't require any input - all values are passed via request metadata. rpc SyncPlatformDetails(identity.unauth.Empty) returns (identity.unauth.Empty) {} + + rpc AddWallet(AddWalletRequest) returns (identity.unauth.Empty) {} } // Helper types @@ -298,3 +300,10 @@ message UserIdentitiesResponse { map identities = 1; } + +// AddWallet + +message AddWalletRequest { + string siwe_message = 1; + string siwe_signature = 2; +} diff --git a/web/protobufs/identity-auth-client.cjs b/web/protobufs/identity-auth-client.cjs --- a/web/protobufs/identity-auth-client.cjs +++ b/web/protobufs/identity-auth-client.cjs @@ -1237,5 +1237,65 @@ }; -module.exports = proto.identity.auth; +/** + * @const + * @type {!grpc.web.MethodDescriptor< + * !proto.identity.auth.AddWalletRequest, + * !proto.identity.unauth.Empty>} + */ +const methodDescriptor_IdentityClientService_AddWallet = new grpc.web.MethodDescriptor( + '/identity.auth.IdentityClientService/AddWallet', + grpc.web.MethodType.UNARY, + proto.identity.auth.AddWalletRequest, + identity_unauth_pb.Empty, + /** + * @param {!proto.identity.auth.AddWalletRequest} request + * @return {!Uint8Array} + */ + function(request) { + return request.serializeBinary(); + }, + identity_unauth_pb.Empty.deserializeBinary +); + +/** + * @param {!proto.identity.auth.AddWalletRequest} request The + * request proto + * @param {?Object} metadata User defined + * call metadata + * @param {function(?grpc.web.RpcError, ?proto.identity.unauth.Empty)} + * callback The callback function(error, response) + * @return {!grpc.web.ClientReadableStream|undefined} + * The XHR Node Readable Stream + */ +proto.identity.auth.IdentityClientServiceClient.prototype.addWallet = + function(request, metadata, callback) { + return this.client_.rpcCall(this.hostname_ + + '/identity.auth.IdentityClientService/AddWallet', + request, + metadata || {}, + methodDescriptor_IdentityClientService_AddWallet, + callback); +}; + + +/** + * @param {!proto.identity.auth.AddWalletRequest} request The + * request proto + * @param {?Object=} metadata User defined + * call metadata + * @return {!Promise} + * Promise that resolves to the response + */ +proto.identity.auth.IdentityClientServicePromiseClient.prototype.addWallet = + function(request, metadata) { + return this.client_.unaryCall(this.hostname_ + + '/identity.auth.IdentityClientService/AddWallet', + request, + metadata || {}, + methodDescriptor_IdentityClientService_AddWallet); +}; + + +module.exports = proto.identity.auth; diff --git a/web/protobufs/identity-auth-client.cjs.flow b/web/protobufs/identity-auth-client.cjs.flow --- a/web/protobufs/identity-auth-client.cjs.flow +++ b/web/protobufs/identity-auth-client.cjs.flow @@ -143,6 +143,13 @@ callback: (err: grpcWeb.RpcError, response: identityStructs.Empty) => void ): grpcWeb.ClientReadableStream; + + addWallet( + request: identityAuthStructs.AddWalletRequest, + metadata: grpcWeb.Metadata | void, + callback: (err: grpcWeb.RpcError, + response: identityStructs.Empty) => void + ): grpcWeb.ClientReadableStream; } declare export class IdentityClientServicePromiseClient { @@ -245,5 +252,9 @@ metadata?: grpcWeb.Metadata ): Promise; -} + addWallet( + request: identityAuthStructs.AddWalletRequest, + metadata?: grpcWeb.Metadata + ): Promise; +} diff --git a/web/protobufs/identity-auth-structs.cjs b/web/protobufs/identity-auth-structs.cjs --- a/web/protobufs/identity-auth-structs.cjs +++ b/web/protobufs/identity-auth-structs.cjs @@ -24,6 +24,7 @@ var identity_unauth_pb = require('./identity-unauth-structs.cjs'); goog.object.extend(proto, identity_unauth_pb); +goog.exportSymbol('proto.identity.auth.AddWalletRequest', null, global); goog.exportSymbol('proto.identity.auth.DeletePasswordUserFinishRequest', null, global); goog.exportSymbol('proto.identity.auth.DeletePasswordUserStartRequest', null, global); goog.exportSymbol('proto.identity.auth.DeletePasswordUserStartResponse', null, global); @@ -618,6 +619,27 @@ */ proto.identity.auth.UserIdentitiesResponse.displayName = 'proto.identity.auth.UserIdentitiesResponse'; } +/** + * Generated by JsPbCodeGenerator. + * @param {Array=} opt_data Optional initial data array, typically from a + * server response, or constructed directly in Javascript. The array is used + * in place and becomes part of the constructed object. It is not cloned. + * If no data is provided, the constructed object will be empty, but still + * valid. + * @extends {jspb.Message} + * @constructor + */ +proto.identity.auth.AddWalletRequest = function(opt_data) { + jspb.Message.initialize(this, opt_data, 0, -1, null, null); +}; +goog.inherits(proto.identity.auth.AddWalletRequest, jspb.Message); +if (goog.DEBUG && !COMPILED) { + /** + * @public + * @override + */ + proto.identity.auth.AddWalletRequest.displayName = 'proto.identity.auth.AddWalletRequest'; +} @@ -5682,4 +5704,164 @@ }; + + + +if (jspb.Message.GENERATE_TO_OBJECT) { +/** + * Creates an object representation of this proto. + * Field names that are reserved in JavaScript and will be renamed to pb_name. + * Optional fields that are not set will be set to undefined. + * To access a reserved field use, foo.pb_, eg, foo.pb_default. + * For the list of reserved names please see: + * net/proto2/compiler/js/internal/generator.cc#kKeyword. + * @param {boolean=} opt_includeInstance Deprecated. whether to include the + * JSPB instance for transitional soy proto support: + * http://goto/soy-param-migration + * @return {!Object} + */ +proto.identity.auth.AddWalletRequest.prototype.toObject = function(opt_includeInstance) { + return proto.identity.auth.AddWalletRequest.toObject(opt_includeInstance, this); +}; + + +/** + * Static version of the {@see toObject} method. + * @param {boolean|undefined} includeInstance Deprecated. Whether to include + * the JSPB instance for transitional soy proto support: + * http://goto/soy-param-migration + * @param {!proto.identity.auth.AddWalletRequest} msg The msg instance to transform. + * @return {!Object} + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.identity.auth.AddWalletRequest.toObject = function(includeInstance, msg) { + var f, obj = { + siweMessage: jspb.Message.getFieldWithDefault(msg, 1, ""), + siweSignature: jspb.Message.getFieldWithDefault(msg, 2, "") + }; + + if (includeInstance) { + obj.$jspbMessageInstance = msg; + } + return obj; +}; +} + + +/** + * Deserializes binary data (in protobuf wire format). + * @param {jspb.ByteSource} bytes The bytes to deserialize. + * @return {!proto.identity.auth.AddWalletRequest} + */ +proto.identity.auth.AddWalletRequest.deserializeBinary = function(bytes) { + var reader = new jspb.BinaryReader(bytes); + var msg = new proto.identity.auth.AddWalletRequest; + return proto.identity.auth.AddWalletRequest.deserializeBinaryFromReader(msg, reader); +}; + + +/** + * Deserializes binary data (in protobuf wire format) from the + * given reader into the given message object. + * @param {!proto.identity.auth.AddWalletRequest} msg The message object to deserialize into. + * @param {!jspb.BinaryReader} reader The BinaryReader to use. + * @return {!proto.identity.auth.AddWalletRequest} + */ +proto.identity.auth.AddWalletRequest.deserializeBinaryFromReader = function(msg, reader) { + while (reader.nextField()) { + if (reader.isEndGroup()) { + break; + } + var field = reader.getFieldNumber(); + switch (field) { + case 1: + var value = /** @type {string} */ (reader.readString()); + msg.setSiweMessage(value); + break; + case 2: + var value = /** @type {string} */ (reader.readString()); + msg.setSiweSignature(value); + break; + default: + reader.skipField(); + break; + } + } + return msg; +}; + + +/** + * Serializes the message to binary data (in protobuf wire format). + * @return {!Uint8Array} + */ +proto.identity.auth.AddWalletRequest.prototype.serializeBinary = function() { + var writer = new jspb.BinaryWriter(); + proto.identity.auth.AddWalletRequest.serializeBinaryToWriter(this, writer); + return writer.getResultBuffer(); +}; + + +/** + * Serializes the given message to binary data (in protobuf wire + * format), writing to the given BinaryWriter. + * @param {!proto.identity.auth.AddWalletRequest} message + * @param {!jspb.BinaryWriter} writer + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.identity.auth.AddWalletRequest.serializeBinaryToWriter = function(message, writer) { + var f = undefined; + f = message.getSiweMessage(); + if (f.length > 0) { + writer.writeString( + 1, + f + ); + } + f = message.getSiweSignature(); + if (f.length > 0) { + writer.writeString( + 2, + f + ); + } +}; + + +/** + * optional string siwe_message = 1; + * @return {string} + */ +proto.identity.auth.AddWalletRequest.prototype.getSiweMessage = function() { + return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 1, "")); +}; + + +/** + * @param {string} value + * @return {!proto.identity.auth.AddWalletRequest} returns this + */ +proto.identity.auth.AddWalletRequest.prototype.setSiweMessage = function(value) { + return jspb.Message.setProto3StringField(this, 1, value); +}; + + +/** + * optional string siwe_signature = 2; + * @return {string} + */ +proto.identity.auth.AddWalletRequest.prototype.getSiweSignature = function() { + return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 2, "")); +}; + + +/** + * @param {string} value + * @return {!proto.identity.auth.AddWalletRequest} returns this + */ +proto.identity.auth.AddWalletRequest.prototype.setSiweSignature = function(value) { + return jspb.Message.setProto3StringField(this, 2, value); +}; + + goog.object.extend(exports, proto.identity.auth); diff --git a/web/protobufs/identity-auth-structs.cjs.flow b/web/protobufs/identity-auth-structs.cjs.flow --- a/web/protobufs/identity-auth-structs.cjs.flow +++ b/web/protobufs/identity-auth-structs.cjs.flow @@ -616,3 +616,23 @@ export type UserIdentitiesResponseObject = { identitiesMap: Array<[string, IdentityObject]>, } + +declare export class AddWalletRequest extends Message { + getSiweMessage(): string; + setSiweMessage(value: string): AddWalletRequest; + + getSiweSignature(): string; + setSiweSignature(value: string): AddWalletRequest; + + serializeBinary(): Uint8Array; + toObject(includeInstance?: boolean): AddWalletRequestObject; + static toObject(includeInstance: boolean, msg: AddWalletRequest): AddWalletRequestObject; + static serializeBinaryToWriter(message: AddWalletRequest, writer: BinaryWriter): void; + static deserializeBinary(bytes: Uint8Array): AddWalletRequest; + static deserializeBinaryFromReader(message: AddWalletRequest, reader: BinaryReader): AddWalletRequest; +} + +export type AddWalletRequestObject = { + siweMessage: string, + siweSignature: string, +}