Skip to content

Commit

Permalink
WIP: stashing changes to move to new laptop
Browse files Browse the repository at this point in the history
  • Loading branch information
ikoenigsknecht committed Mar 12, 2024
1 parent d292fb5 commit fac75e8
Show file tree
Hide file tree
Showing 19 changed files with 3,014 additions and 2,767 deletions.
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@
"publish": "lerna version $npm_config_release --no-private",
"postpublish": "node copy-changelog.js && git add . && git commit -m 'Update packages CHANGELOG.md' && git push",
"start:desktop": "lerna run --scope @quiet/desktop start",
"lint:all": "lerna run lint"
"lint:all": "lerna run lint",
"distAndTest:local": "lerna run --scope @quiet/desktop dist:local && lerna run --scope e2e-tests test"
},
"engines": {
"node": "18.12.1",
Expand Down
Binary file modified packages/.DS_Store
Binary file not shown.
1 change: 1 addition & 0 deletions packages/backend/src/nest/socket/socket.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,7 @@ export class SocketService extends EventEmitter implements OnModuleInit {
// ====== Users ======

socket.on(SocketActionTypes.SET_USER_PROFILE, (profile: UserProfile) => {
this.logger(profile.profile.photo)
this.emit(SocketActionTypes.SET_USER_PROFILE, profile)
})
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,59 +9,7 @@ import { getCrypto, PublicKeyInfo } from 'pkijs'
import { ChannelMessage, NoCryptoEngineError, PublicChannel, UserProfile } from '@quiet/types'
import { configCrypto, generateKeyPair, sign } from '@quiet/identity'

import { isPng, base64DataURLToByteArray, UserProfileStore, UserProfileKeyValueIndex } from './userProfile.store'

describe('UserProfileStore/isPng', () => {
test('returns true for a valid PNG', () => {
// Bytes in decimal copied out of a PNG file
// e.g. od -t u1 ~/Pictures/test.png | less
const png = new Uint8Array([137, 80, 78, 71, 13, 10, 26, 10, 0, 0, 0, 13, 73, 72, 68, 82])
expect(isPng(png)).toBeTruthy()
})

test('returns false for a invalid PNG', () => {
// Changed the first byte from 137 to 136
const png = new Uint8Array([136, 80, 78, 71, 13, 10, 26, 10, 0, 0, 0, 13, 73, 72, 68, 82])
expect(isPng(png)).toBeFalsy()
})

test('returns false for a incomplete PNG', () => {
// Removed last byte from the PNG header
const png = new Uint8Array([137, 80, 78, 71, 13, 10, 26])
expect(isPng(png)).toBeFalsy()
})
})

describe('UserProfileStore/base64DataURLToByteArray', () => {
test("throws error if data URL prefix doesn't exist", () => {
const contents = '1234567'
expect(() => base64DataURLToByteArray(contents)).toThrow(Error)
})

test('throws error if data URL prefix is malformatted', () => {
let contents = ',1234567'
expect(() => base64DataURLToByteArray(contents)).toThrow(Error)

contents = ',1234567'
expect(() => base64DataURLToByteArray(contents)).toThrow(Error)

contents = 'data:,1234567'
expect(() => base64DataURLToByteArray(contents)).toThrow(Error)

contents = ';base64,1234567'
expect(() => base64DataURLToByteArray(contents)).toThrow(Error)

contents = 'dat:;base64,1234567'
expect(() => base64DataURLToByteArray(contents)).toThrow(Error)
})

test('returns Uint8Array if data URL prefix is correct', () => {
// base64 encoding of binary 'm'
// btoa('m') == 'bQ=='
const contents = 'data:mime;base64,bQ=='
expect(base64DataURLToByteArray(contents)).toEqual(new Uint8Array(['m'.charCodeAt(0)]))
})
})
import { UserProfileStore, UserProfileKeyValueIndex } from './userProfile.store'

const getUserProfile = async ({
pngByteArray,
Expand Down
110 changes: 1 addition & 109 deletions packages/backend/src/nest/storage/userProfile/userProfile.store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,118 +17,10 @@ import createLogger from '../../common/logger'
import { OrbitDb } from '../orbitDb/orbitDb.service'
import { StorageEvents } from '../storage.types'
import { KeyValueIndex } from '../orbitDb/keyValueIndex'
import { validatePhoto } from './userProfile.utils'

const logger = createLogger('UserProfileStore')

export const checkImgHeader = (buffer: Uint8Array, header: number[]): boolean => {
if (buffer.length < header.length) {
return false
}

for (let i = 0; i < header.length; i++) {
if (buffer[i] !== header[i]) {
return false
}
}
return true
}

/**
* Check magic byte sequence to determine if buffer is a PNG image.
*/
export const isPng = (buffer: Uint8Array): boolean => {
// https://en.wikipedia.org/wiki/PNG
const pngHeader = [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]

return checkImgHeader(buffer, pngHeader)
}

/**
* Check magic byte sequence to determine if buffer is a JPEG image.
*/
export const isJpeg = (buffer: Uint8Array): boolean => {
// https://en.wikipedia.org/wiki/JPEG
const jpegHeader = [0xff, 0xd8, 0xff]

return checkImgHeader(buffer, jpegHeader)
}

/**
* Check magic byte sequence to determine if buffer is a GIF image.
*/
export const isGif = (buffer: Uint8Array): boolean => {
// https://en.wikipedia.org/wiki/GIF
// GIF images are different from JPEG and PNG in that there are two slightly different magic number sequences that translate to GIF89a and GIF87a
const gifHeader89 = [0x47, 0x49, 0x46, 0x38, 0x39, 0x61]
const gifHeader87 = [0x47, 0x49, 0x46, 0x38, 0x37, 0x61]
const headers = [gifHeader89, gifHeader87]

for (const header of headers) {
if (checkImgHeader(buffer, header)) {
return true
}
}

return false
}

/**
* Validate a profile photo in a user profile
*
* @param photoString Base64 string representing the photo file that was uploaded
* @param pubKey Public key string for logging purposes
* @returns True if photo is valid and false if not
*/
export const validatePhoto = (photoString: string, pubKey: string): boolean => {
// validate that we have the photo as a base64 string
if (typeof photoString !== 'string') {
logger.error('Expected PNG, JPEG or GIF as base64 string for user profile photo', pubKey)
return false
}

const photoBytes = base64DataURLToByteArray(photoString)

// validate that the type is approved and has a matching magic number header
if (
!(photoString.startsWith('data:image/png;base64,') && isPng(photoBytes)) &&
!(photoString.startsWith('data:image/jpeg;base64,') && isJpeg(photoBytes)) &&
!(photoString.startsWith('data:image/gif;base64,') && isGif(photoBytes))
) {
logger.error('Expected valid PNG, JPEG or GIF for user profile photo', pubKey)
return false
}

// 200 KB = 204800 B limit
//
// TODO: Perhaps the compression matters and we should check
// actual dimensions in pixels?
if (photoBytes.length > 204800) {
logger.error('User profile photo must be less than or equal to 200KB')
return false
}

return true
}

/**
* Takes a base64 data URI string that starts with 'data:*\/*;base64,'
* as returned from
* https://developer.mozilla.org/en-US/docs/Web/API/FileReader/readAsDataURL
* and converts it to a Uint8Array.
*/
export const base64DataURLToByteArray = (contents: string): Uint8Array => {
const [header, base64Data] = contents.split(',')
if (!header.startsWith('data:') || !header.endsWith(';base64')) {
throw new Error('Expected base64 data URI')
}
const chars = atob(base64Data)
const bytes = new Array(chars.length)
for (let i = 0; i < chars.length; i++) {
bytes[i] = chars.charCodeAt(i)
}
return new Uint8Array(bytes)
}

@Injectable()
export class UserProfileStore extends EventEmitter {
public store: KeyValueStore<UserProfile>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { describe, expect, test } from '@jest/globals'

import { isPng, base64DataURLToByteArray } from './userProfile.utils'

describe('isPng', () => {
test('returns true for a valid PNG', () => {
// Bytes in decimal copied out of a PNG file
// e.g. od -t u1 ~/Pictures/test.png | less
const png = new Uint8Array([137, 80, 78, 71, 13, 10, 26, 10, 0, 0, 0, 13, 73, 72, 68, 82])
expect(isPng(png)).toBeTruthy()
})

test('returns false for a invalid PNG', () => {
// Changed the first byte from 137 to 136
const png = new Uint8Array([136, 80, 78, 71, 13, 10, 26, 10, 0, 0, 0, 13, 73, 72, 68, 82])
expect(isPng(png)).toBeFalsy()
})

test('returns false for a incomplete PNG', () => {
// Removed last byte from the PNG header
const png = new Uint8Array([137, 80, 78, 71, 13, 10, 26])
expect(isPng(png)).toBeFalsy()
})
})

describe('base64DataURLToByteArray', () => {
test("throws error if data URL prefix doesn't exist", () => {
const contents = '1234567'
expect(() => base64DataURLToByteArray(contents)).toThrow(Error)
})

test('throws error if data URL prefix is malformatted', () => {
let contents = ',1234567'
expect(() => base64DataURLToByteArray(contents)).toThrow(Error)

contents = ',1234567'
expect(() => base64DataURLToByteArray(contents)).toThrow(Error)

contents = 'data:,1234567'
expect(() => base64DataURLToByteArray(contents)).toThrow(Error)

contents = ';base64,1234567'
expect(() => base64DataURLToByteArray(contents)).toThrow(Error)

contents = 'dat:;base64,1234567'
expect(() => base64DataURLToByteArray(contents)).toThrow(Error)
})

test('returns Uint8Array if data URL prefix is correct', () => {
// base64 encoding of binary 'm'
// btoa('m') == 'bQ=='
const contents = 'data:mime;base64,bQ=='
expect(base64DataURLToByteArray(contents)).toEqual(new Uint8Array(['m'.charCodeAt(0)]))
})
})
112 changes: 112 additions & 0 deletions packages/backend/src/nest/storage/userProfile/userProfile.utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import createLogger from '../../common/logger'

const logger = createLogger('UserProfileStoreUtils')

export const checkImgHeader = (buffer: Uint8Array, header: number[]): boolean => {
if (buffer.length < header.length) {
return false
}

for (let i = 0; i < header.length; i++) {
if (buffer[i] !== header[i]) {
return false
}
}
return true
}

/**
* Check magic byte sequence to determine if buffer is a PNG image.
*/
export const isPng = (buffer: Uint8Array): boolean => {
// https://en.wikipedia.org/wiki/PNG
const pngHeader = [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]

return checkImgHeader(buffer, pngHeader)
}

/**
* Check magic byte sequence to determine if buffer is a JPEG image.
*/
export const isJpeg = (buffer: Uint8Array): boolean => {
// https://en.wikipedia.org/wiki/JPEG
const jpegHeader = [0xff, 0xd8, 0xff]

return checkImgHeader(buffer, jpegHeader)
}

/**
* Check magic byte sequence to determine if buffer is a GIF image.
*/
export const isGif = (buffer: Uint8Array): boolean => {
// https://en.wikipedia.org/wiki/GIF
// GIF images are different from JPEG and PNG in that there are two slightly different magic number sequences that translate to GIF89a and GIF87a
const gifHeader89 = [0x47, 0x49, 0x46, 0x38, 0x39, 0x61]
const gifHeader87 = [0x47, 0x49, 0x46, 0x38, 0x37, 0x61]
const headers = [gifHeader89, gifHeader87]

for (const header of headers) {
if (checkImgHeader(buffer, header)) {
return true
}
}

return false
}

/**
* Validate a profile photo in a user profile
*
* @param photoString Base64 string representing the photo file that was uploaded
* @param pubKey Public key string for logging purposes
* @returns True if photo is valid and false if not
*/
export const validatePhoto = (photoString: string, pubKey: string): boolean => {
// validate that we have the photo as a base64 string
if (typeof photoString !== 'string') {
logger.error('Expected PNG, JPEG or GIF as base64 string for user profile photo', pubKey)
return false
}

const photoBytes = base64DataURLToByteArray(photoString)

// validate that the type is approved and has a matching magic number header
if (
!(photoString.startsWith('data:image/png;base64,') && isPng(photoBytes)) &&
!(photoString.startsWith('data:image/jpeg;base64,') && isJpeg(photoBytes)) &&
!(photoString.startsWith('data:image/gif;base64,') && isGif(photoBytes))
) {
logger.error('Expected valid PNG, JPEG or GIF for user profile photo', pubKey)
return false
}

// 200 KB = 204800 B limit
//
// TODO: Perhaps the compression matters and we should check
// actual dimensions in pixels?
if (photoBytes.length > 204800) {
logger.error('User profile photo must be less than or equal to 200KB')
return false
}

return true
}

/**
* Takes a base64 data URI string that starts with 'data:*\/*;base64,'
* as returned from
* https://developer.mozilla.org/en-US/docs/Web/API/FileReader/readAsDataURL
* and converts it to a Uint8Array.
*/
export const base64DataURLToByteArray = (contents: string): Uint8Array => {
const [header, base64Data] = contents.split(',')
if (!header.startsWith('data:') || !header.endsWith(';base64')) {
throw new Error('Expected base64 data URI')
}
const chars = atob(base64Data)
const bytes = new Array(chars.length)
for (let i = 0; i < chars.length; i++) {
bytes[i] = chars.charCodeAt(i)
}
return new Uint8Array(bytes)
}
Loading

0 comments on commit fac75e8

Please sign in to comment.