Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

validate encoded data #544

Merged
merged 3 commits into from
Apr 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/bright-students-taste.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@status-im/js': patch
---

validate encoded data
157 changes: 156 additions & 1 deletion packages/status-js/src/utils/encode-url-data.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,34 @@ describe('Encode URL data', () => {
expect(decodedData).toEqual(data)
})

test('should encode and decode channel', () => {
test('should throw for invalid community data', () => {
expect(() => {
const encodedData = 'Ow=='
decodeCommunityURLData(encodedData)
}).toThrowError()
})

test('should throw for unsupported data length', () => {
expect(() => {
const encodedData =
'G2QBQJwFdqwxrBnNb57kP0irrJpuouIjS1WZqHS6A2txojsUHidyu3evaAO3GQQku5NCQXiwAYchBIMNyptts=MD9bZAwoTasraIMkjbS1uAD7oxsAQ53OAmQWCefyBuuXlAu6J7eKQRQhgg5tan75fFp9jwGIjBLbGhnyUht2qj5GWlSBp7_OXsHxgnr21xA2HgR9VGYYikQJA4tcQHDrQzg_ARC9KiOVDD6vgTCM9_CN0HJ1zxwP3w6nzgkDTNuvDCFD3Clqo6Cf_UNY2cNRlKTqj86G4gC2dUNSApwiq72BdGTtrleiRFPUhCbTRbmEG4YwFOs4EjBdJHHRiqjS5GYGc1dAdgcGr2BQ==============================================================================================================================================='
decodeCommunityURLData(encodedData)
}).toThrowErrorMatchingInlineSnapshot(`
"[
{
\\"code\\": \\"too_big\\",
\\"maximum\\": 500,
\\"type\\": \\"string\\",
\\"inclusive\\": true,
\\"exact\\": false,
\\"message\\": \\"String must contain at most 500 character(s)\\",
\\"path\\": []
}
]"
`)
})

test('should encode and decode channel', () => {
const data = {
emoji: '🏴󠁧󠁢󠁥󠁮󠁧󠁿',
displayName: 'lorem-ipsum-dolore-nulla',
Expand All @@ -56,6 +83,70 @@ describe('Encode URL data', () => {
expect(decodedData).toEqual(data)
})

test('should throw for invalid channel data', () => {
expect(() => {
const encodedData = 'Ow=='
decodeChannelURLData(encodedData)
}).toThrowErrorMatchingInlineSnapshot(`
"[
{
\\"code\\": \\"invalid_type\\",
\\"expected\\": \\"string\\",
\\"received\\": \\"undefined\\",
\\"path\\": [
\\"displayName\\"
],
\\"message\\": \\"Required\\"
},
{
\\"code\\": \\"invalid_type\\",
\\"expected\\": \\"string\\",
\\"received\\": \\"undefined\\",
\\"path\\": [
\\"description\\"
],
\\"message\\": \\"Required\\"
},
{
\\"code\\": \\"invalid_type\\",
\\"expected\\": \\"string\\",
\\"received\\": \\"undefined\\",
\\"path\\": [
\\"emoji\\"
],
\\"message\\": \\"Required\\"
},
{
\\"code\\": \\"invalid_type\\",
\\"expected\\": \\"string\\",
\\"received\\": \\"undefined\\",
\\"path\\": [
\\"color\\"
],
\\"message\\": \\"Required\\"
},
{
\\"code\\": \\"invalid_type\\",
\\"expected\\": \\"object\\",
\\"received\\": \\"undefined\\",
\\"path\\": [
\\"community\\"
],
\\"message\\": \\"Required\\"
},
{
\\"code\\": \\"invalid_type\\",
\\"expected\\": \\"string\\",
\\"received\\": \\"undefined\\",
\\"path\\": [
\\"uuid\\"
],
\\"message\\": \\"Required\\"
}
]"
`)
})

test('should encode and decode user', () => {
const data = {
displayName: 'Lorem ipsum dolore nulla',
Expand All @@ -72,4 +163,68 @@ describe('Encode URL data', () => {
)
expect(decodedData).toEqual(data)
})

test('should throw for invalid user data', () => {
expect(() => {
const encodedData = 'Ow=='
decodeChannelURLData(encodedData)
}).toThrowErrorMatchingInlineSnapshot(`
"[
{
\\"code\\": \\"invalid_type\\",
\\"expected\\": \\"string\\",
\\"received\\": \\"undefined\\",
\\"path\\": [
\\"displayName\\"
],
\\"message\\": \\"Required\\"
},
{
\\"code\\": \\"invalid_type\\",
\\"expected\\": \\"string\\",
\\"received\\": \\"undefined\\",
\\"path\\": [
\\"description\\"
],
\\"message\\": \\"Required\\"
},
{
\\"code\\": \\"invalid_type\\",
\\"expected\\": \\"string\\",
\\"received\\": \\"undefined\\",
\\"path\\": [
\\"emoji\\"
],
\\"message\\": \\"Required\\"
},
{
\\"code\\": \\"invalid_type\\",
\\"expected\\": \\"string\\",
\\"received\\": \\"undefined\\",
\\"path\\": [
\\"color\\"
],
\\"message\\": \\"Required\\"
},
{
\\"code\\": \\"invalid_type\\",
\\"expected\\": \\"object\\",
\\"received\\": \\"undefined\\",
\\"path\\": [
\\"community\\"
],
\\"message\\": \\"Required\\"
},
{
\\"code\\": \\"invalid_type\\",
\\"expected\\": \\"string\\",
\\"received\\": \\"undefined\\",
\\"path\\": [
\\"uuid\\"
],
\\"message\\": \\"Required\\"
}
]"
`)
})
})
58 changes: 50 additions & 8 deletions packages/status-js/src/utils/encode-url-data.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,24 @@
import { base64url } from '@scure/base'
import { brotliCompressSync, brotliDecompressSync } from 'zlib'
import { z } from 'zod'

import { Channel, Community, URLData, User } from '../protos/url_pb'

import type { PlainMessage } from '@bufbuild/protobuf'

export type EncodedURLData = string & { _: 'EncodedURLData' }

const colorSchema = z.string().regex(/^#[0-9A-Fa-f]{6}$/)
const communityDisplayName = z.string().max(30).nonempty()

const communitySchema = z.object({
displayName: communityDisplayName,
description: z.string().max(140).nonempty(),
membersCount: z.number().nonnegative(),
color: colorSchema,
tagIndices: z.number().nonnegative().array(),
})

export function encodeCommunityURLData(
data: PlainMessage<Community>
): EncodedURLData {
Expand All @@ -17,33 +29,58 @@ export function encodeCommunityURLData(
export function decodeCommunityURLData(data: string): PlainMessage<Community> {
const deserialized = decodeURLData(data)

return Community.fromBinary(
deserialized.content
).toJson() as PlainMessage<Community>
const community = Community.fromBinary(deserialized.content).toJson()

return communitySchema.parse(community)
}

const channelSchema = z.object({
displayName: z.string().max(24).nonempty(),
description: z.string().max(140).nonempty(),
emoji: z.string().emoji(),
color: colorSchema,
community: z.object({
displayName: communityDisplayName,
}),
uuid: z.string().uuid(),
})

export function encodeChannelURLData(
data: PlainMessage<Channel>
): EncodedURLData {
return encodeURLData(new Channel(data).toBinary()) as EncodedURLData
}

export function decodeChannelURLData(data: string): PlainMessage<Channel> {
export function decodeChannelURLData(data: string): Omit<
PlainMessage<Channel>,
'community'
> & {
community: Pick<PlainMessage<Community>, 'displayName'>
} {
const deserialized = decodeURLData(data)

return Channel.fromBinary(
deserialized.content
).toJson() as PlainMessage<Channel>
const channel = Channel.fromBinary(deserialized.content).toJson()

return channelSchema.parse(channel)
}

const userSchema = z.object({
displayName: z.string().max(24).nonempty(),
description: z.string().max(240).nonempty(),
// fixme: await integration in native platforms
color: colorSchema.optional().default('#ffffff'),
})

export function encodeUserURLData(data: PlainMessage<User>): EncodedURLData {
return encodeURLData(new User(data).toBinary()) as EncodedURLData
}

export function decodeUserURLData(data: string): PlainMessage<User> {
const deserialized = decodeURLData(data)

return User.fromBinary(deserialized.content).toJson() as PlainMessage<User>
const user = User.fromBinary(deserialized.content).toJson()

return userSchema.parse(user)
}

function encodeURLData(data: Uint8Array): string {
Expand All @@ -57,6 +94,11 @@ function encodeURLData(data: Uint8Array): string {
}

function decodeURLData(data: string): URLData {
// note: https://github.com/status-im/status-web/pull/345#discussion_r1113129396 observed lengths
// note?: https://docs.google.com/spreadsheets/d/1JD4kp0aUm90piUZ7FgM_c2NGe2PdN8BFB11wmt5UZIY/view#gid=1260088614 limit for url path segmets not split by ";" or "_"
// fixme: set to 301 per url path segment when the above mentioned splitting is implemented
z.string().max(500).parse(data) // default max in order not to compute arbitrary values

const decoded = base64url.decode(data)
const decompressed = brotliDecompressSync(decoded)
const deserialized = URLData.fromBinary(decompressed)
Expand Down
Loading