From 1e00da6e2f5eba204e5693a460945223caf7d27b Mon Sep 17 00:00:00 2001 From: bmarty Date: Mon, 2 Oct 2023 00:19:14 +0000 Subject: [PATCH 01/61] Sync Emojis --- .../emoji_picker_datasource_formatted.json | 411 +++++++++++++++--- .../main/res/raw/emoji_picker_datasource.json | 2 +- 2 files changed, 341 insertions(+), 72 deletions(-) diff --git a/tools/emojis/emoji_picker_datasource_formatted.json b/tools/emojis/emoji_picker_datasource_formatted.json index 839c91ff0e..1166641395 100644 --- a/tools/emojis/emoji_picker_datasource_formatted.json +++ b/tools/emojis/emoji_picker_datasource_formatted.json @@ -55,6 +55,8 @@ "face-exhaling", "lying-face", "shaking-face", + "head-shaking-horizontally", + "head-shaking-vertically", "relieved-face", "pensive-face", "sleepy-face", @@ -419,24 +421,42 @@ "person-walking", "man-walking", "woman-walking", + "person-walking-facing-right", + "woman-walking-facing-right", + "man-walking-facing-right", "person-standing", "man-standing", "woman-standing", "person-kneeling", "man-kneeling", "woman-kneeling", + "person-kneeling-facing-right", + "woman-kneeling-facing-right", + "man-kneeling-facing-right", "person-with-white-cane", + "person-with-white-cane-facing-right", "man-with-white-cane", + "man-with-white-cane-facing-right", "woman-with-white-cane", + "woman-with-white-cane-facing-right", "person-in-motorized-wheelchair", + "person-in-motorized-wheelchair-facing-right", "man-in-motorized-wheelchair", + "man-in-motorized-wheelchair-facing-right", "woman-in-motorized-wheelchair", + "woman-in-motorized-wheelchair-facing-right", "person-in-manual-wheelchair", + "person-in-manual-wheelchair-facing-right", "man-in-manual-wheelchair", + "man-in-manual-wheelchair-facing-right", "woman-in-manual-wheelchair", + "woman-in-manual-wheelchair-facing-right", "person-running", "man-running", "woman-running", + "person-running-facing-right", + "woman-running-facing-right", + "man-running-facing-right", "woman-dancing", "man-dancing", "person-in-suit-levitating", @@ -509,7 +529,6 @@ "couple-with-heart-woman-man", "couple-with-heart-man-man", "couple-with-heart-woman-woman", - "family", "family-man-woman-boy", "family-man-woman-girl", "family-man-woman-girl-boy", @@ -539,6 +558,11 @@ "bust-in-silhouette", "busts-in-silhouette", "people-hugging", + "family", + "family-adult-adult-child", + "family-adult-adult-child-child", + "family-adult-child", + "family-adult-child-child", "footprints" ] }, @@ -633,6 +657,7 @@ "wing", "black-bird", "goose", + "phoenix", "frog", "crocodile", "turtle", @@ -709,6 +734,7 @@ "watermelon", "tangerine", "lemon", + "lime", "banana", "pineapple", "mango", @@ -740,6 +766,7 @@ "chestnut", "ginger-root", "pea-pod", + "brown-mushroom", "bread", "croissant", "baguette-bread", @@ -1366,6 +1393,7 @@ "balance-scale", "white-cane", "link", + "broken-chain", "chains", "hook", "toolbox", @@ -1893,7 +1921,7 @@ "flag-turkmenistan", "flag-tunisia", "flag-tonga", - "flag-turkey", + "flag-trkiye", "flag-trinidad--tobago", "flag-tuvalu", "flag-taiwan", @@ -2432,6 +2460,7 @@ "j": [ "face", "mouth", + "zip", "zipper", "zipper-mouth face", "zipper_mouth_face", @@ -2450,7 +2479,8 @@ "mild surprise", "scepticism", "face", - "surprise" + "surprise", + "suspicious" ] }, "neutral-face": { @@ -2486,8 +2516,7 @@ "face", "mouth", "quiet", - "silent", - "hellokitty" + "silent" ] }, "dotted-line-face": { @@ -2544,6 +2573,7 @@ "unimpressed", "skeptical", "dubious", + "ugh", "side_eye" ] }, @@ -2592,7 +2622,7 @@ ] }, "shaking-face": { - "a": "⊛ Shaking Face", + "a": "Shaking Face", "b": "1FAE8", "j": [ "earthquake", @@ -2604,6 +2634,24 @@ "blurry" ] }, + "head-shaking-horizontally": { + "a": "⊛ Head Shaking Horizontally", + "b": "1F642-200D-2194-FE0F", + "j": [ + "head shaking horizontally", + "no", + "shake" + ] + }, + "head-shaking-vertically": { + "a": "⊛ Head Shaking Vertically", + "b": "1F642-200D-2195-FE0F", + "j": [ + "head shaking vertically", + "nod", + "yes" + ] + }, "relieved-face": { "a": "Relieved Face", "b": "1F60C", @@ -3117,6 +3165,7 @@ "sad", "sob", "tear", + "sobbing", "tears", "upset", "depressed" @@ -3795,7 +3844,7 @@ ] }, "pink-heart": { - "a": "⊛ Pink Heart", + "a": "Pink Heart", "b": "1FA77", "j": [ "cute", @@ -3851,7 +3900,7 @@ ] }, "light-blue-heart": { - "a": "⊛ Light Blue Heart", + "a": "Light Blue Heart", "b": "1FA75", "j": [ "cyan", @@ -3892,7 +3941,7 @@ ] }, "grey-heart": { - "a": "⊛ Grey Heart", + "a": "Grey Heart", "b": "1FA76", "j": [ "gray", @@ -4197,7 +4246,7 @@ ] }, "leftwards-pushing-hand": { - "a": "⊛ Leftwards Pushing Hand", + "a": "Leftwards Pushing Hand", "b": "1FAF7", "j": [ "high five", @@ -4211,7 +4260,7 @@ ] }, "rightwards-pushing-hand": { - "a": "⊛ Rightwards Pushing Hand", + "a": "Rightwards Pushing Hand", "b": "1FAF8", "j": [ "high five", @@ -4611,9 +4660,11 @@ "manicure", "nail", "polish", + "nail_care", "beauty", "finger", - "fashion" + "fashion", + "slay" ] }, "selfie": { @@ -5561,6 +5612,7 @@ "b": "1F9D1-200D-1F3EB", "j": [ "instructor", + "lecturer", "professor" ] }, @@ -5569,6 +5621,7 @@ "b": "1F468-200D-1F3EB", "j": [ "instructor", + "lecturer", "man", "professor", "teacher", @@ -5580,6 +5633,7 @@ "b": "1F469-200D-1F3EB", "j": [ "instructor", + "lecturer", "professor", "teacher", "woman", @@ -5591,8 +5645,8 @@ "b": "1F9D1-200D-2696-FE0F", "j": [ "justice", - "scales", - "law" + "law", + "scales" ] }, "man-judge": { @@ -5601,6 +5655,7 @@ "j": [ "judge", "justice", + "law", "man", "scales", "court", @@ -5613,6 +5668,7 @@ "j": [ "judge", "justice", + "law", "scales", "woman", "court", @@ -6006,8 +6062,8 @@ "a": "Firefighter", "b": "1F9D1-200D-1F692", "j": [ - "firetruck", - "fire" + "fire", + "firetruck" ] }, "man-firefighter": { @@ -6482,8 +6538,8 @@ "a": "Mx Claus", "b": "1F9D1-200D-1F384", "j": [ - "Claus, christmas", - "christmas" + "christmas", + "claus" ] }, "superhero": { @@ -6881,6 +6937,27 @@ "female" ] }, + "person-walking-facing-right": { + "a": "⊛ Person Walking Facing Right", + "b": "1F6B6-200D-27A1-FE0F", + "j": [ + "" + ] + }, + "woman-walking-facing-right": { + "a": "⊛ Woman Walking Facing Right", + "b": "1F6B6-200D-2640-FE0F-200D-27A1-FE0F", + "j": [ + "" + ] + }, + "man-walking-facing-right": { + "a": "⊛ Man Walking Facing Right", + "b": "1F6B6-200D-2642-FE0F-200D-27A1-FE0F", + "j": [ + "" + ] + }, "person-standing": { "a": "Person Standing", "b": "1F9CD", @@ -6938,6 +7015,27 @@ "pray" ] }, + "person-kneeling-facing-right": { + "a": "⊛ Person Kneeling Facing Right", + "b": "1F9CE-200D-27A1-FE0F", + "j": [ + "" + ] + }, + "woman-kneeling-facing-right": { + "a": "⊛ Woman Kneeling Facing Right", + "b": "1F9CE-200D-2640-FE0F-200D-27A1-FE0F", + "j": [ + "" + ] + }, + "man-kneeling-facing-right": { + "a": "⊛ Man Kneeling Facing Right", + "b": "1F9CE-200D-2642-FE0F-200D-27A1-FE0F", + "j": [ + "" + ] + }, "person-with-white-cane": { "a": "Person with White Cane", "b": "1F9D1-200D-1F9AF", @@ -6947,6 +7045,13 @@ "person_with_probing_cane" ] }, + "person-with-white-cane-facing-right": { + "a": "⊛ Person with White Cane Facing Right", + "b": "1F9D1-200D-1F9AF-200D-27A1-FE0F", + "j": [ + "" + ] + }, "man-with-white-cane": { "a": "Man with White Cane", "b": "1F468-200D-1F9AF", @@ -6957,6 +7062,13 @@ "man_with_probing_cane" ] }, + "man-with-white-cane-facing-right": { + "a": "⊛ Man with White Cane Facing Right", + "b": "1F468-200D-1F9AF-200D-27A1-FE0F", + "j": [ + "" + ] + }, "woman-with-white-cane": { "a": "Woman with White Cane", "b": "1F469-200D-1F9AF", @@ -6967,6 +7079,13 @@ "woman_with_probing_cane" ] }, + "woman-with-white-cane-facing-right": { + "a": "⊛ Woman with White Cane Facing Right", + "b": "1F469-200D-1F9AF-200D-27A1-FE0F", + "j": [ + "" + ] + }, "person-in-motorized-wheelchair": { "a": "Person in Motorized Wheelchair", "b": "1F9D1-200D-1F9BC", @@ -6976,6 +7095,13 @@ "disability" ] }, + "person-in-motorized-wheelchair-facing-right": { + "a": "⊛ Person in Motorized Wheelchair Facing Right", + "b": "1F9D1-200D-1F9BC-200D-27A1-FE0F", + "j": [ + "" + ] + }, "man-in-motorized-wheelchair": { "a": "Man in Motorized Wheelchair", "b": "1F468-200D-1F9BC", @@ -6986,6 +7112,13 @@ "disability" ] }, + "man-in-motorized-wheelchair-facing-right": { + "a": "⊛ Man in Motorized Wheelchair Facing Right", + "b": "1F468-200D-1F9BC-200D-27A1-FE0F", + "j": [ + "" + ] + }, "woman-in-motorized-wheelchair": { "a": "Woman in Motorized Wheelchair", "b": "1F469-200D-1F9BC", @@ -6996,6 +7129,13 @@ "disability" ] }, + "woman-in-motorized-wheelchair-facing-right": { + "a": "⊛ Woman in Motorized Wheelchair Facing Right", + "b": "1F469-200D-1F9BC-200D-27A1-FE0F", + "j": [ + "" + ] + }, "person-in-manual-wheelchair": { "a": "Person in Manual Wheelchair", "b": "1F9D1-200D-1F9BD", @@ -7005,6 +7145,13 @@ "disability" ] }, + "person-in-manual-wheelchair-facing-right": { + "a": "⊛ Person in Manual Wheelchair Facing Right", + "b": "1F9D1-200D-1F9BD-200D-27A1-FE0F", + "j": [ + "" + ] + }, "man-in-manual-wheelchair": { "a": "Man in Manual Wheelchair", "b": "1F468-200D-1F9BD", @@ -7015,6 +7162,13 @@ "disability" ] }, + "man-in-manual-wheelchair-facing-right": { + "a": "⊛ Man in Manual Wheelchair Facing Right", + "b": "1F468-200D-1F9BD-200D-27A1-FE0F", + "j": [ + "" + ] + }, "woman-in-manual-wheelchair": { "a": "Woman in Manual Wheelchair", "b": "1F469-200D-1F9BD", @@ -7025,6 +7179,13 @@ "disability" ] }, + "woman-in-manual-wheelchair-facing-right": { + "a": "⊛ Woman in Manual Wheelchair Facing Right", + "b": "1F469-200D-1F9BD-200D-27A1-FE0F", + "j": [ + "" + ] + }, "person-running": { "a": "Person Running", "b": "1F3C3", @@ -7061,6 +7222,27 @@ "female" ] }, + "person-running-facing-right": { + "a": "⊛ Person Running Facing Right", + "b": "1F3C3-200D-27A1-FE0F", + "j": [ + "" + ] + }, + "woman-running-facing-right": { + "a": "⊛ Woman Running Facing Right", + "b": "1F3C3-200D-2640-FE0F-200D-27A1-FE0F", + "j": [ + "" + ] + }, + "man-running-facing-right": { + "a": "⊛ Man Running Facing Right", + "b": "1F3C3-200D-2642-FE0F-200D-27A1-FE0F", + "j": [ + "" + ] + }, "woman-dancing": { "a": "Woman Dancing", "b": "1F483", @@ -7929,21 +8111,6 @@ "marriage" ] }, - "family": { - "a": "Family", - "b": "1F46A-FE0F", - "j": [ - "home", - "parents", - "child", - "mom", - "dad", - "father", - "mother", - "people", - "human" - ] - }, "family-man-woman-boy": { "a": "Family: Man, Woman, Boy", "b": "1F468-200D-1F469-200D-1F466", @@ -8352,6 +8519,49 @@ "care" ] }, + "family": { + "a": "Family", + "b": "1F46A-FE0F", + "j": [ + "home", + "parents", + "child", + "mom", + "dad", + "father", + "mother", + "people", + "human" + ] + }, + "family-adult-adult-child": { + "a": "⊛ Family: Adult, Adult, Child", + "b": "1F9D1-200D-1F9D1-200D-1F9D2", + "j": [ + "family: adult, adult, child" + ] + }, + "family-adult-adult-child-child": { + "a": "⊛ Family: Adult, Adult, Child, Child", + "b": "1F9D1-200D-1F9D1-200D-1F9D2-200D-1F9D2", + "j": [ + "family: adult, adult, child, child" + ] + }, + "family-adult-child": { + "a": "⊛ Family: Adult, Child", + "b": "1F9D1-200D-1F9D2", + "j": [ + "family: adult, child" + ] + }, + "family-adult-child-child": { + "a": "⊛ Family: Adult, Child, Child", + "b": "1F9D1-200D-1F9D2-200D-1F9D2", + "j": [ + "family: adult, child, child" + ] + }, "footprints": { "a": "Footprints", "b": "1F463", @@ -8620,7 +8830,7 @@ ] }, "moose": { - "a": "⊛ Moose", + "a": "Moose", "b": "1FACE", "j": [ "animal", @@ -8635,7 +8845,7 @@ ] }, "donkey": { - "a": "⊛ Donkey", + "a": "Donkey", "b": "1FACF", "j": [ "animal", @@ -9086,7 +9296,6 @@ "a": "Kangaroo", "b": "1F998", "j": [ - "Australia", "joey", "jump", "marsupial", @@ -9314,7 +9523,7 @@ ] }, "wing": { - "a": "⊛ Wing", + "a": "Wing", "b": "1FABD", "j": [ "angelic", @@ -9327,7 +9536,7 @@ ] }, "black-bird": { - "a": "⊛ Black Bird", + "a": "Black Bird", "b": "1F426-200D-2B1B", "j": [ "bird", @@ -9338,7 +9547,7 @@ ] }, "goose": { - "a": "⊛ Goose", + "a": "Goose", "b": "1FABF", "j": [ "bird", @@ -9349,6 +9558,17 @@ "goosebumps" ] }, + "phoenix": { + "a": "⊛ Phoenix", + "b": "1F426-200D-1F525", + "j": [ + "fantasy", + "firebird", + "phoenix", + "rebirth", + "reincarnation" + ] + }, "frog": { "a": "Frog", "b": "1F438", @@ -9588,7 +9808,7 @@ ] }, "jellyfish": { - "a": "⊛ Jellyfish", + "a": "Jellyfish", "b": "1FABC", "j": [ "burn", @@ -9810,9 +10030,7 @@ "Buddhism", "flower", "Hinduism", - "India", "purity", - "Vietnam", "calm", "meditation" ] @@ -9894,7 +10112,7 @@ ] }, "hyacinth": { - "a": "⊛ Hyacinth", + "a": "Hyacinth", "b": "1FABB", "j": [ "bluebonnet", @@ -10140,6 +10358,16 @@ "nature" ] }, + "lime": { + "a": "⊛ Lime", + "b": "1F34B-200D-1F7E9", + "j": [ + "citrus", + "fruit", + "lime", + "tropical" + ] + }, "banana": { "a": "Banana", "b": "1F34C", @@ -10432,7 +10660,7 @@ ] }, "ginger-root": { - "a": "⊛ Ginger Root", + "a": "Ginger Root", "b": "1FADA", "j": [ "beer", @@ -10444,7 +10672,7 @@ ] }, "pea-pod": { - "a": "⊛ Pea Pod", + "a": "Pea Pod", "b": "1FADB", "j": [ "beans", @@ -10457,6 +10685,17 @@ "green" ] }, + "brown-mushroom": { + "a": "⊛ Brown Mushroom", + "b": "1F344-200D-1F7EB", + "j": [ + "brown mushroom", + "food", + "fungus", + "nature", + "vegetable" + ] + }, "bread": { "a": "Bread", "b": "1F35E", @@ -10863,7 +11102,8 @@ "rice", "food", "japanese", - "snack" + "snack", + "senbei" ] }, "rice-ball": { @@ -10874,7 +11114,9 @@ "Japanese", "rice", "food", - "japanese" + "japanese", + "onigiri", + "omusubi" ] }, "cooked-rice": { @@ -11251,7 +11493,8 @@ "dessert", "pudding", "sweet", - "food" + "food", + "flan" ] }, "honey-pot": { @@ -12302,6 +12545,7 @@ "j": [ "amusement park", "play", + "theme park", "fun", "park" ] @@ -12312,6 +12556,7 @@ "j": [ "amusement park", "ferris", + "theme park", "wheel", "photo", "carnival", @@ -12325,6 +12570,7 @@ "amusement park", "coaster", "roller", + "theme park", "carnival", "playground", "photo", @@ -15543,7 +15789,7 @@ ] }, "folding-hand-fan": { - "a": "⊛ Folding Hand Fan", + "a": "Folding Hand Fan", "b": "1FAAD", "j": [ "cooling", @@ -15730,7 +15976,7 @@ ] }, "hair-pick": { - "a": "⊛ Hair Pick", + "a": "Hair Pick", "b": "1FAAE", "j": [ "Afro", @@ -16179,7 +16425,7 @@ ] }, "maracas": { - "a": "⊛ Maracas", + "a": "Maracas", "b": "1FA87", "j": [ "instrument", @@ -16190,7 +16436,7 @@ ] }, "flute": { - "a": "⊛ Flute", + "a": "Flute", "b": "1FA88", "j": [ "fife", @@ -17613,7 +17859,6 @@ "a": "Boomerang", "b": "1FA83", "j": [ - "australia", "rebound", "repercussion", "weapon" @@ -17735,6 +17980,18 @@ "url" ] }, + "broken-chain": { + "a": "⊛ Broken Chain", + "b": "26D3-FE0F-200D-1F4A5", + "j": [ + "break", + "breaking", + "broken chain", + "chain", + "cuffs", + "freedom" + ] + }, "chains": { "a": "Chains", "b": "26D3-FE0F", @@ -19053,7 +19310,7 @@ ] }, "khanda": { - "a": "⊛ Khanda", + "a": "Khanda", "b": "1FAAF", "j": [ "religion", @@ -19474,12 +19731,13 @@ ] }, "wireless": { - "a": "⊛ Wireless", + "a": "Wireless", "b": "1F6DC", "j": [ "computer", "internet", "network", + "wi-fi", "wifi", "contactless", "signal" @@ -20019,7 +20277,8 @@ "0", "numbers", "blue-square", - "null" + "null", + "zero" ] }, "keycap-1": { @@ -20029,7 +20288,8 @@ "keycap", "blue-square", "numbers", - "1" + "1", + "one" ] }, "keycap-2": { @@ -20040,7 +20300,8 @@ "numbers", "2", "prime", - "blue-square" + "blue-square", + "two" ] }, "keycap-3": { @@ -20051,7 +20312,8 @@ "3", "numbers", "prime", - "blue-square" + "blue-square", + "three" ] }, "keycap-4": { @@ -20061,7 +20323,8 @@ "keycap", "4", "numbers", - "blue-square" + "blue-square", + "four" ] }, "keycap-5": { @@ -20072,7 +20335,8 @@ "5", "numbers", "blue-square", - "prime" + "prime", + "five" ] }, "keycap-6": { @@ -20082,7 +20346,8 @@ "keycap", "6", "numbers", - "blue-square" + "blue-square", + "six" ] }, "keycap-7": { @@ -20093,7 +20358,8 @@ "7", "numbers", "blue-square", - "prime" + "prime", + "seven" ] }, "keycap-8": { @@ -20103,7 +20369,8 @@ "keycap", "8", "blue-square", - "numbers" + "numbers", + "eight" ] }, "keycap-9": { @@ -20113,7 +20380,8 @@ "keycap", "blue-square", "numbers", - "9" + "9", + "nine" ] }, "keycap-10": { @@ -20123,7 +20391,8 @@ "keycap", "numbers", "10", - "blue-square" + "blue-square", + "ten" ] }, "input-latin-uppercase": { @@ -21091,12 +21360,10 @@ "flag", "gay", "lgbt", - "glbt", "queer", "homosexual", "lesbian", - "bisexual", - "transgender" + "bisexual" ] }, "transgender-flag": { @@ -21108,6 +21375,7 @@ "pink", "transgender", "white", + "pride", "lgbtq" ] }, @@ -23961,11 +24229,12 @@ "tonga" ] }, - "flag-turkey": { - "a": "Flag: Turkey", + "flag-trkiye": { + "a": "Flag: Türkiye", "b": "1F1F9-1F1F7", "j": [ "flag", + "flag_turkey", "turkey", "nation", "country", diff --git a/vector/src/main/res/raw/emoji_picker_datasource.json b/vector/src/main/res/raw/emoji_picker_datasource.json index 6548208126..2c66fd5a8a 100644 --- a/vector/src/main/res/raw/emoji_picker_datasource.json +++ b/vector/src/main/res/raw/emoji_picker_datasource.json @@ -1 +1 @@ -{"compressed":true,"categories":[{"id":"smileys_&_emotion","name":"Smileys & Emotion","emojis":["grinning-face","grinning-face-with-big-eyes","grinning-face-with-smiling-eyes","beaming-face-with-smiling-eyes","grinning-squinting-face","grinning-face-with-sweat","rolling-on-the-floor-laughing","face-with-tears-of-joy","slightly-smiling-face","upsidedown-face","melting-face","winking-face","smiling-face-with-smiling-eyes","smiling-face-with-halo","smiling-face-with-hearts","smiling-face-with-hearteyes","starstruck","face-blowing-a-kiss","kissing-face","smiling-face","kissing-face-with-closed-eyes","kissing-face-with-smiling-eyes","smiling-face-with-tear","face-savoring-food","face-with-tongue","winking-face-with-tongue","zany-face","squinting-face-with-tongue","moneymouth-face","smiling-face-with-open-hands","face-with-hand-over-mouth","face-with-open-eyes-and-hand-over-mouth","face-with-peeking-eye","shushing-face","thinking-face","saluting-face","zippermouth-face","face-with-raised-eyebrow","neutral-face","expressionless-face","face-without-mouth","dotted-line-face","face-in-clouds","smirking-face","unamused-face","face-with-rolling-eyes","grimacing-face","face-exhaling","lying-face","shaking-face","relieved-face","pensive-face","sleepy-face","drooling-face","sleeping-face","face-with-medical-mask","face-with-thermometer","face-with-headbandage","nauseated-face","face-vomiting","sneezing-face","hot-face","cold-face","woozy-face","face-with-crossedout-eyes","face-with-spiral-eyes","exploding-head","cowboy-hat-face","partying-face","disguised-face","smiling-face-with-sunglasses","nerd-face","face-with-monocle","confused-face","face-with-diagonal-mouth","worried-face","slightly-frowning-face","frowning-face","face-with-open-mouth","hushed-face","astonished-face","flushed-face","pleading-face","face-holding-back-tears","frowning-face-with-open-mouth","anguished-face","fearful-face","anxious-face-with-sweat","sad-but-relieved-face","crying-face","loudly-crying-face","face-screaming-in-fear","confounded-face","persevering-face","disappointed-face","downcast-face-with-sweat","weary-face","tired-face","yawning-face","face-with-steam-from-nose","enraged-face","angry-face","face-with-symbols-on-mouth","smiling-face-with-horns","angry-face-with-horns","skull","skull-and-crossbones","pile-of-poo","clown-face","ogre","goblin","ghost","alien","alien-monster","robot","grinning-cat","grinning-cat-with-smiling-eyes","cat-with-tears-of-joy","smiling-cat-with-hearteyes","cat-with-wry-smile","kissing-cat","weary-cat","crying-cat","pouting-cat","seenoevil-monkey","hearnoevil-monkey","speaknoevil-monkey","love-letter","heart-with-arrow","heart-with-ribbon","sparkling-heart","growing-heart","beating-heart","revolving-hearts","two-hearts","heart-decoration","heart-exclamation","broken-heart","heart-on-fire","mending-heart","red-heart","pink-heart","orange-heart","yellow-heart","green-heart","blue-heart","light-blue-heart","purple-heart","brown-heart","black-heart","grey-heart","white-heart","kiss-mark","hundred-points","anger-symbol","collision","dizzy","sweat-droplets","dashing-away","hole","speech-balloon","eye-in-speech-bubble","left-speech-bubble","right-anger-bubble","thought-balloon","zzz"]},{"id":"people_&_body","name":"People & Body","emojis":["waving-hand","raised-back-of-hand","hand-with-fingers-splayed","raised-hand","vulcan-salute","rightwards-hand","leftwards-hand","palm-down-hand","palm-up-hand","leftwards-pushing-hand","rightwards-pushing-hand","ok-hand","pinched-fingers","pinching-hand","victory-hand","crossed-fingers","hand-with-index-finger-and-thumb-crossed","loveyou-gesture","sign-of-the-horns","call-me-hand","backhand-index-pointing-left","backhand-index-pointing-right","backhand-index-pointing-up","middle-finger","backhand-index-pointing-down","index-pointing-up","index-pointing-at-the-viewer","thumbs-up","thumbs-down","raised-fist","oncoming-fist","leftfacing-fist","rightfacing-fist","clapping-hands","raising-hands","heart-hands","open-hands","palms-up-together","handshake","folded-hands","writing-hand","nail-polish","selfie","flexed-biceps","mechanical-arm","mechanical-leg","leg","foot","ear","ear-with-hearing-aid","nose","brain","anatomical-heart","lungs","tooth","bone","eyes","eye","tongue","mouth","biting-lip","baby","child","boy","girl","person","person-blond-hair","man","person-beard","man-beard","woman-beard","man-red-hair","man-curly-hair","man-white-hair","man-bald","woman","woman-red-hair","person-red-hair","woman-curly-hair","person-curly-hair","woman-white-hair","person-white-hair","woman-bald","person-bald","woman-blond-hair","man-blond-hair","older-person","old-man","old-woman","person-frowning","man-frowning","woman-frowning","person-pouting","man-pouting","woman-pouting","person-gesturing-no","man-gesturing-no","woman-gesturing-no","person-gesturing-ok","man-gesturing-ok","woman-gesturing-ok","person-tipping-hand","man-tipping-hand","woman-tipping-hand","person-raising-hand","man-raising-hand","woman-raising-hand","deaf-person","deaf-man","deaf-woman","person-bowing","man-bowing","woman-bowing","person-facepalming","man-facepalming","woman-facepalming","person-shrugging","man-shrugging","woman-shrugging","health-worker","man-health-worker","woman-health-worker","student","man-student","woman-student","teacher","man-teacher","woman-teacher","judge","man-judge","woman-judge","farmer","man-farmer","woman-farmer","cook","man-cook","woman-cook","mechanic","man-mechanic","woman-mechanic","factory-worker","man-factory-worker","woman-factory-worker","office-worker","man-office-worker","woman-office-worker","scientist","man-scientist","woman-scientist","technologist","man-technologist","woman-technologist","singer","man-singer","woman-singer","artist","man-artist","woman-artist","pilot","man-pilot","woman-pilot","astronaut","man-astronaut","woman-astronaut","firefighter","man-firefighter","woman-firefighter","police-officer","man-police-officer","woman-police-officer","detective","man-detective","woman-detective","guard","man-guard","woman-guard","ninja","construction-worker","man-construction-worker","woman-construction-worker","person-with-crown","prince","princess","person-wearing-turban","man-wearing-turban","woman-wearing-turban","person-with-skullcap","woman-with-headscarf","person-in-tuxedo","man-in-tuxedo","woman-in-tuxedo","person-with-veil","man-with-veil","woman-with-veil","pregnant-woman","pregnant-man","pregnant-person","breastfeeding","woman-feeding-baby","man-feeding-baby","person-feeding-baby","baby-angel","santa-claus","mrs-claus","mx-claus","superhero","man-superhero","woman-superhero","supervillain","man-supervillain","woman-supervillain","mage","man-mage","woman-mage","fairy","man-fairy","woman-fairy","vampire","man-vampire","woman-vampire","merperson","merman","mermaid","elf","man-elf","woman-elf","genie","man-genie","woman-genie","zombie","man-zombie","woman-zombie","troll","person-getting-massage","man-getting-massage","woman-getting-massage","person-getting-haircut","man-getting-haircut","woman-getting-haircut","person-walking","man-walking","woman-walking","person-standing","man-standing","woman-standing","person-kneeling","man-kneeling","woman-kneeling","person-with-white-cane","man-with-white-cane","woman-with-white-cane","person-in-motorized-wheelchair","man-in-motorized-wheelchair","woman-in-motorized-wheelchair","person-in-manual-wheelchair","man-in-manual-wheelchair","woman-in-manual-wheelchair","person-running","man-running","woman-running","woman-dancing","man-dancing","person-in-suit-levitating","people-with-bunny-ears","men-with-bunny-ears","women-with-bunny-ears","person-in-steamy-room","man-in-steamy-room","woman-in-steamy-room","person-climbing","man-climbing","woman-climbing","person-fencing","horse-racing","skier","snowboarder","person-golfing","man-golfing","woman-golfing","person-surfing","man-surfing","woman-surfing","person-rowing-boat","man-rowing-boat","woman-rowing-boat","person-swimming","man-swimming","woman-swimming","person-bouncing-ball","man-bouncing-ball","woman-bouncing-ball","person-lifting-weights","man-lifting-weights","woman-lifting-weights","person-biking","man-biking","woman-biking","person-mountain-biking","man-mountain-biking","woman-mountain-biking","person-cartwheeling","man-cartwheeling","woman-cartwheeling","people-wrestling","men-wrestling","women-wrestling","person-playing-water-polo","man-playing-water-polo","woman-playing-water-polo","person-playing-handball","man-playing-handball","woman-playing-handball","person-juggling","man-juggling","woman-juggling","person-in-lotus-position","man-in-lotus-position","woman-in-lotus-position","person-taking-bath","person-in-bed","people-holding-hands","women-holding-hands","woman-and-man-holding-hands","men-holding-hands","kiss","kiss-woman-man","kiss-man-man","kiss-woman-woman","couple-with-heart","couple-with-heart-woman-man","couple-with-heart-man-man","couple-with-heart-woman-woman","family","family-man-woman-boy","family-man-woman-girl","family-man-woman-girl-boy","family-man-woman-boy-boy","family-man-woman-girl-girl","family-man-man-boy","family-man-man-girl","family-man-man-girl-boy","family-man-man-boy-boy","family-man-man-girl-girl","family-woman-woman-boy","family-woman-woman-girl","family-woman-woman-girl-boy","family-woman-woman-boy-boy","family-woman-woman-girl-girl","family-man-boy","family-man-boy-boy","family-man-girl","family-man-girl-boy","family-man-girl-girl","family-woman-boy","family-woman-boy-boy","family-woman-girl","family-woman-girl-boy","family-woman-girl-girl","speaking-head","bust-in-silhouette","busts-in-silhouette","people-hugging","footprints"]},{"id":"animals_&_nature","name":"Animals & Nature","emojis":["monkey-face","monkey","gorilla","orangutan","dog-face","dog","guide-dog","service-dog","poodle","wolf","fox","raccoon","cat-face","cat","black-cat","lion","tiger-face","tiger","leopard","horse-face","moose","donkey","horse","unicorn","zebra","deer","bison","cow-face","ox","water-buffalo","cow","pig-face","pig","boar","pig-nose","ram","ewe","goat","camel","twohump-camel","llama","giraffe","elephant","mammoth","rhinoceros","hippopotamus","mouse-face","mouse","rat","hamster","rabbit-face","rabbit","chipmunk","beaver","hedgehog","bat","bear","polar-bear","koala","panda","sloth","otter","skunk","kangaroo","badger","paw-prints","turkey","chicken","rooster","hatching-chick","baby-chick","frontfacing-baby-chick","bird","penguin","dove","eagle","duck","swan","owl","dodo","feather","flamingo","peacock","parrot","wing","black-bird","goose","frog","crocodile","turtle","lizard","snake","dragon-face","dragon","sauropod","trex","spouting-whale","whale","dolphin","seal","fish","tropical-fish","blowfish","shark","octopus","spiral-shell","coral","jellyfish","snail","butterfly","bug","ant","honeybee","beetle","lady-beetle","cricket","cockroach","spider","spider-web","scorpion","mosquito","fly","worm","microbe","bouquet","cherry-blossom","white-flower","lotus","rosette","rose","wilted-flower","hibiscus","sunflower","blossom","tulip","hyacinth","seedling","potted-plant","evergreen-tree","deciduous-tree","palm-tree","cactus","sheaf-of-rice","herb","shamrock","four-leaf-clover","maple-leaf","fallen-leaf","leaf-fluttering-in-wind","empty-nest","nest-with-eggs","mushroom"]},{"id":"food_&_drink","name":"Food & Drink","emojis":["grapes","melon","watermelon","tangerine","lemon","banana","pineapple","mango","red-apple","green-apple","pear","peach","cherries","strawberry","blueberries","kiwi-fruit","tomato","olive","coconut","avocado","eggplant","potato","carrot","ear-of-corn","hot-pepper","bell-pepper","cucumber","leafy-green","broccoli","garlic","onion","peanuts","beans","chestnut","ginger-root","pea-pod","bread","croissant","baguette-bread","flatbread","pretzel","bagel","pancakes","waffle","cheese-wedge","meat-on-bone","poultry-leg","cut-of-meat","bacon","hamburger","french-fries","pizza","hot-dog","sandwich","taco","burrito","tamale","stuffed-flatbread","falafel","egg","cooking","shallow-pan-of-food","pot-of-food","fondue","bowl-with-spoon","green-salad","popcorn","butter","salt","canned-food","bento-box","rice-cracker","rice-ball","cooked-rice","curry-rice","steaming-bowl","spaghetti","roasted-sweet-potato","oden","sushi","fried-shrimp","fish-cake-with-swirl","moon-cake","dango","dumpling","fortune-cookie","takeout-box","crab","lobster","shrimp","squid","oyster","soft-ice-cream","shaved-ice","ice-cream","doughnut","cookie","birthday-cake","shortcake","cupcake","pie","chocolate-bar","candy","lollipop","custard","honey-pot","baby-bottle","glass-of-milk","hot-beverage","teapot","teacup-without-handle","sake","bottle-with-popping-cork","wine-glass","cocktail-glass","tropical-drink","beer-mug","clinking-beer-mugs","clinking-glasses","tumbler-glass","pouring-liquid","cup-with-straw","bubble-tea","beverage-box","mate","ice","chopsticks","fork-and-knife-with-plate","fork-and-knife","spoon","kitchen-knife","jar","amphora"]},{"id":"travel_&_places","name":"Travel & Places","emojis":["globe-showing-europeafrica","globe-showing-americas","globe-showing-asiaaustralia","globe-with-meridians","world-map","map-of-japan","compass","snowcapped-mountain","mountain","volcano","mount-fuji","camping","beach-with-umbrella","desert","desert-island","national-park","stadium","classical-building","building-construction","brick","rock","wood","hut","houses","derelict-house","house","house-with-garden","office-building","japanese-post-office","post-office","hospital","bank","hotel","love-hotel","convenience-store","school","department-store","factory","japanese-castle","castle","wedding","tokyo-tower","statue-of-liberty","church","mosque","hindu-temple","synagogue","shinto-shrine","kaaba","fountain","tent","foggy","night-with-stars","cityscape","sunrise-over-mountains","sunrise","cityscape-at-dusk","sunset","bridge-at-night","hot-springs","carousel-horse","playground-slide","ferris-wheel","roller-coaster","barber-pole","circus-tent","locomotive","railway-car","highspeed-train","bullet-train","train","metro","light-rail","station","tram","monorail","mountain-railway","tram-car","bus","oncoming-bus","trolleybus","minibus","ambulance","fire-engine","police-car","oncoming-police-car","taxi","oncoming-taxi","automobile","oncoming-automobile","sport-utility-vehicle","pickup-truck","delivery-truck","articulated-lorry","tractor","racing-car","motorcycle","motor-scooter","manual-wheelchair","motorized-wheelchair","auto-rickshaw","bicycle","kick-scooter","skateboard","roller-skate","bus-stop","motorway","railway-track","oil-drum","fuel-pump","wheel","police-car-light","horizontal-traffic-light","vertical-traffic-light","stop-sign","construction","anchor","ring-buoy","sailboat","canoe","speedboat","passenger-ship","ferry","motor-boat","ship","airplane","small-airplane","airplane-departure","airplane-arrival","parachute","seat","helicopter","suspension-railway","mountain-cableway","aerial-tramway","satellite","rocket","flying-saucer","bellhop-bell","luggage","hourglass-done","hourglass-not-done","watch","alarm-clock","stopwatch","timer-clock","mantelpiece-clock","twelve-oclock","twelvethirty","one-oclock","onethirty","two-oclock","twothirty","three-oclock","threethirty","four-oclock","fourthirty","five-oclock","fivethirty","six-oclock","sixthirty","seven-oclock","seventhirty","eight-oclock","eightthirty","nine-oclock","ninethirty","ten-oclock","tenthirty","eleven-oclock","eleventhirty","new-moon","waxing-crescent-moon","first-quarter-moon","waxing-gibbous-moon","full-moon","waning-gibbous-moon","last-quarter-moon","waning-crescent-moon","crescent-moon","new-moon-face","first-quarter-moon-face","last-quarter-moon-face","thermometer","sun","full-moon-face","sun-with-face","ringed-planet","star","glowing-star","shooting-star","milky-way","cloud","sun-behind-cloud","cloud-with-lightning-and-rain","sun-behind-small-cloud","sun-behind-large-cloud","sun-behind-rain-cloud","cloud-with-rain","cloud-with-snow","cloud-with-lightning","tornado","fog","wind-face","cyclone","rainbow","closed-umbrella","umbrella","umbrella-with-rain-drops","umbrella-on-ground","high-voltage","snowflake","snowman","snowman-without-snow","comet","fire","droplet","water-wave"]},{"id":"activities","name":"Activities","emojis":["jackolantern","christmas-tree","fireworks","sparkler","firecracker","sparkles","balloon","party-popper","confetti-ball","tanabata-tree","pine-decoration","japanese-dolls","carp-streamer","wind-chime","moon-viewing-ceremony","red-envelope","ribbon","wrapped-gift","reminder-ribbon","admission-tickets","ticket","military-medal","trophy","sports-medal","1st-place-medal","2nd-place-medal","3rd-place-medal","soccer-ball","baseball","softball","basketball","volleyball","american-football","rugby-football","tennis","flying-disc","bowling","cricket-game","field-hockey","ice-hockey","lacrosse","ping-pong","badminton","boxing-glove","martial-arts-uniform","goal-net","flag-in-hole","ice-skate","fishing-pole","diving-mask","running-shirt","skis","sled","curling-stone","bullseye","yoyo","kite","water-pistol","pool-8-ball","crystal-ball","magic-wand","video-game","joystick","slot-machine","game-die","puzzle-piece","teddy-bear","piata","mirror-ball","nesting-dolls","spade-suit","heart-suit","diamond-suit","club-suit","chess-pawn","joker","mahjong-red-dragon","flower-playing-cards","performing-arts","framed-picture","artist-palette","thread","sewing-needle","yarn","knot"]},{"id":"objects","name":"Objects","emojis":["glasses","sunglasses","goggles","lab-coat","safety-vest","necktie","tshirt","jeans","scarf","gloves","coat","socks","dress","kimono","sari","onepiece-swimsuit","briefs","shorts","bikini","womans-clothes","folding-hand-fan","purse","handbag","clutch-bag","shopping-bags","backpack","thong-sandal","mans-shoe","running-shoe","hiking-boot","flat-shoe","highheeled-shoe","womans-sandal","ballet-shoes","womans-boot","hair-pick","crown","womans-hat","top-hat","graduation-cap","billed-cap","military-helmet","rescue-workers-helmet","prayer-beads","lipstick","ring","gem-stone","muted-speaker","speaker-low-volume","speaker-medium-volume","speaker-high-volume","loudspeaker","megaphone","postal-horn","bell","bell-with-slash","musical-score","musical-note","musical-notes","studio-microphone","level-slider","control-knobs","microphone","headphone","radio","saxophone","accordion","guitar","musical-keyboard","trumpet","violin","banjo","drum","long-drum","maracas","flute","mobile-phone","mobile-phone-with-arrow","telephone","telephone-receiver","pager","fax-machine","battery","low-battery","electric-plug","laptop","desktop-computer","printer","keyboard","computer-mouse","trackball","computer-disk","floppy-disk","optical-disk","dvd","abacus","movie-camera","film-frames","film-projector","clapper-board","television","camera","camera-with-flash","video-camera","videocassette","magnifying-glass-tilted-left","magnifying-glass-tilted-right","candle","light-bulb","flashlight","red-paper-lantern","diya-lamp","notebook-with-decorative-cover","closed-book","open-book","green-book","blue-book","orange-book","books","notebook","ledger","page-with-curl","scroll","page-facing-up","newspaper","rolledup-newspaper","bookmark-tabs","bookmark","label","money-bag","coin","yen-banknote","dollar-banknote","euro-banknote","pound-banknote","money-with-wings","credit-card","receipt","chart-increasing-with-yen","envelope","email","incoming-envelope","envelope-with-arrow","outbox-tray","inbox-tray","package","closed-mailbox-with-raised-flag","closed-mailbox-with-lowered-flag","open-mailbox-with-raised-flag","open-mailbox-with-lowered-flag","postbox","ballot-box-with-ballot","pencil","black-nib","fountain-pen","pen","paintbrush","crayon","memo","briefcase","file-folder","open-file-folder","card-index-dividers","calendar","tearoff-calendar","spiral-notepad","spiral-calendar","card-index","chart-increasing","chart-decreasing","bar-chart","clipboard","pushpin","round-pushpin","paperclip","linked-paperclips","straight-ruler","triangular-ruler","scissors","card-file-box","file-cabinet","wastebasket","locked","unlocked","locked-with-pen","locked-with-key","key","old-key","hammer","axe","pick","hammer-and-pick","hammer-and-wrench","dagger","crossed-swords","bomb","boomerang","bow-and-arrow","shield","carpentry-saw","wrench","screwdriver","nut-and-bolt","gear","clamp","balance-scale","white-cane","link","chains","hook","toolbox","magnet","ladder","alembic","test-tube","petri-dish","dna","microscope","telescope","satellite-antenna","syringe","drop-of-blood","pill","adhesive-bandage","crutch","stethoscope","xray","door","elevator","mirror","window","bed","couch-and-lamp","chair","toilet","plunger","shower","bathtub","mouse-trap","razor","lotion-bottle","safety-pin","broom","basket","roll-of-paper","bucket","soap","bubbles","toothbrush","sponge","fire-extinguisher","shopping-cart","cigarette","coffin","headstone","funeral-urn","nazar-amulet","hamsa","moai","placard","identification-card"]},{"id":"symbols","name":"Symbols","emojis":["atm-sign","litter-in-bin-sign","potable-water","wheelchair-symbol","mens-room","womens-room","restroom","baby-symbol","water-closet","passport-control","customs","baggage-claim","left-luggage","warning","children-crossing","no-entry","prohibited","no-bicycles","no-smoking","no-littering","nonpotable-water","no-pedestrians","no-mobile-phones","no-one-under-eighteen","radioactive","biohazard","up-arrow","upright-arrow","right-arrow","downright-arrow","down-arrow","downleft-arrow","left-arrow","upleft-arrow","updown-arrow","leftright-arrow","right-arrow-curving-left","left-arrow-curving-right","right-arrow-curving-up","right-arrow-curving-down","clockwise-vertical-arrows","counterclockwise-arrows-button","back-arrow","end-arrow","on-arrow","soon-arrow","top-arrow","place-of-worship","atom-symbol","om","star-of-david","wheel-of-dharma","yin-yang","latin-cross","orthodox-cross","star-and-crescent","peace-symbol","menorah","dotted-sixpointed-star","khanda","aries","taurus","gemini","cancer","leo","virgo","libra","scorpio","sagittarius","capricorn","aquarius","pisces","ophiuchus","shuffle-tracks-button","repeat-button","repeat-single-button","play-button","fastforward-button","next-track-button","play-or-pause-button","reverse-button","fast-reverse-button","last-track-button","upwards-button","fast-up-button","downwards-button","fast-down-button","pause-button","stop-button","record-button","eject-button","cinema","dim-button","bright-button","antenna-bars","wireless","vibration-mode","mobile-phone-off","female-sign","male-sign","transgender-symbol","multiply","plus","minus","divide","heavy-equals-sign","infinity","double-exclamation-mark","exclamation-question-mark","red-question-mark","white-question-mark","white-exclamation-mark","red-exclamation-mark","wavy-dash","currency-exchange","heavy-dollar-sign","medical-symbol","recycling-symbol","fleurdelis","trident-emblem","name-badge","japanese-symbol-for-beginner","hollow-red-circle","check-mark-button","check-box-with-check","check-mark","cross-mark","cross-mark-button","curly-loop","double-curly-loop","part-alternation-mark","eightspoked-asterisk","eightpointed-star","sparkle","copyright","registered","trade-mark","keycap","keycap","keycap-0","keycap-1","keycap-2","keycap-3","keycap-4","keycap-5","keycap-6","keycap-7","keycap-8","keycap-9","keycap-10","input-latin-uppercase","input-latin-lowercase","input-numbers","input-symbols","input-latin-letters","a-button-blood-type","ab-button-blood-type","b-button-blood-type","cl-button","cool-button","free-button","information","id-button","circled-m","new-button","ng-button","o-button-blood-type","ok-button","p-button","sos-button","up-button","vs-button","japanese-here-button","japanese-service-charge-button","japanese-monthly-amount-button","japanese-not-free-of-charge-button","japanese-reserved-button","japanese-bargain-button","japanese-discount-button","japanese-free-of-charge-button","japanese-prohibited-button","japanese-acceptable-button","japanese-application-button","japanese-passing-grade-button","japanese-vacancy-button","japanese-congratulations-button","japanese-secret-button","japanese-open-for-business-button","japanese-no-vacancy-button","red-circle","orange-circle","yellow-circle","green-circle","blue-circle","purple-circle","brown-circle","black-circle","white-circle","red-square","orange-square","yellow-square","green-square","blue-square","purple-square","brown-square","black-large-square","white-large-square","black-medium-square","white-medium-square","black-mediumsmall-square","white-mediumsmall-square","black-small-square","white-small-square","large-orange-diamond","large-blue-diamond","small-orange-diamond","small-blue-diamond","red-triangle-pointed-up","red-triangle-pointed-down","diamond-with-a-dot","radio-button","white-square-button","black-square-button"]},{"id":"flags","name":"Flags","emojis":["chequered-flag","triangular-flag","crossed-flags","black-flag","white-flag","rainbow-flag","transgender-flag","pirate-flag","flag-ascension-island","flag-andorra","flag-united-arab-emirates","flag-afghanistan","flag-antigua--barbuda","flag-anguilla","flag-albania","flag-armenia","flag-angola","flag-antarctica","flag-argentina","flag-american-samoa","flag-austria","flag-australia","flag-aruba","flag-land-islands","flag-azerbaijan","flag-bosnia--herzegovina","flag-barbados","flag-bangladesh","flag-belgium","flag-burkina-faso","flag-bulgaria","flag-bahrain","flag-burundi","flag-benin","flag-st-barthlemy","flag-bermuda","flag-brunei","flag-bolivia","flag-caribbean-netherlands","flag-brazil","flag-bahamas","flag-bhutan","flag-bouvet-island","flag-botswana","flag-belarus","flag-belize","flag-canada","flag-cocos-keeling-islands","flag-congo--kinshasa","flag-central-african-republic","flag-congo--brazzaville","flag-switzerland","flag-cte-divoire","flag-cook-islands","flag-chile","flag-cameroon","flag-china","flag-colombia","flag-clipperton-island","flag-costa-rica","flag-cuba","flag-cape-verde","flag-curaao","flag-christmas-island","flag-cyprus","flag-czechia","flag-germany","flag-diego-garcia","flag-djibouti","flag-denmark","flag-dominica","flag-dominican-republic","flag-algeria","flag-ceuta--melilla","flag-ecuador","flag-estonia","flag-egypt","flag-western-sahara","flag-eritrea","flag-spain","flag-ethiopia","flag-european-union","flag-finland","flag-fiji","flag-falkland-islands","flag-micronesia","flag-faroe-islands","flag-france","flag-gabon","flag-united-kingdom","flag-grenada","flag-georgia","flag-french-guiana","flag-guernsey","flag-ghana","flag-gibraltar","flag-greenland","flag-gambia","flag-guinea","flag-guadeloupe","flag-equatorial-guinea","flag-greece","flag-south-georgia--south-sandwich-islands","flag-guatemala","flag-guam","flag-guineabissau","flag-guyana","flag-hong-kong-sar-china","flag-heard--mcdonald-islands","flag-honduras","flag-croatia","flag-haiti","flag-hungary","flag-canary-islands","flag-indonesia","flag-ireland","flag-israel","flag-isle-of-man","flag-india","flag-british-indian-ocean-territory","flag-iraq","flag-iran","flag-iceland","flag-italy","flag-jersey","flag-jamaica","flag-jordan","flag-japan","flag-kenya","flag-kyrgyzstan","flag-cambodia","flag-kiribati","flag-comoros","flag-st-kitts--nevis","flag-north-korea","flag-south-korea","flag-kuwait","flag-cayman-islands","flag-kazakhstan","flag-laos","flag-lebanon","flag-st-lucia","flag-liechtenstein","flag-sri-lanka","flag-liberia","flag-lesotho","flag-lithuania","flag-luxembourg","flag-latvia","flag-libya","flag-morocco","flag-monaco","flag-moldova","flag-montenegro","flag-st-martin","flag-madagascar","flag-marshall-islands","flag-north-macedonia","flag-mali","flag-myanmar-burma","flag-mongolia","flag-macao-sar-china","flag-northern-mariana-islands","flag-martinique","flag-mauritania","flag-montserrat","flag-malta","flag-mauritius","flag-maldives","flag-malawi","flag-mexico","flag-malaysia","flag-mozambique","flag-namibia","flag-new-caledonia","flag-niger","flag-norfolk-island","flag-nigeria","flag-nicaragua","flag-netherlands","flag-norway","flag-nepal","flag-nauru","flag-niue","flag-new-zealand","flag-oman","flag-panama","flag-peru","flag-french-polynesia","flag-papua-new-guinea","flag-philippines","flag-pakistan","flag-poland","flag-st-pierre--miquelon","flag-pitcairn-islands","flag-puerto-rico","flag-palestinian-territories","flag-portugal","flag-palau","flag-paraguay","flag-qatar","flag-runion","flag-romania","flag-serbia","flag-russia","flag-rwanda","flag-saudi-arabia","flag-solomon-islands","flag-seychelles","flag-sudan","flag-sweden","flag-singapore","flag-st-helena","flag-slovenia","flag-svalbard--jan-mayen","flag-slovakia","flag-sierra-leone","flag-san-marino","flag-senegal","flag-somalia","flag-suriname","flag-south-sudan","flag-so-tom--prncipe","flag-el-salvador","flag-sint-maarten","flag-syria","flag-eswatini","flag-tristan-da-cunha","flag-turks--caicos-islands","flag-chad","flag-french-southern-territories","flag-togo","flag-thailand","flag-tajikistan","flag-tokelau","flag-timorleste","flag-turkmenistan","flag-tunisia","flag-tonga","flag-turkey","flag-trinidad--tobago","flag-tuvalu","flag-taiwan","flag-tanzania","flag-ukraine","flag-uganda","flag-us-outlying-islands","flag-united-nations","flag-united-states","flag-uruguay","flag-uzbekistan","flag-vatican-city","flag-st-vincent--grenadines","flag-venezuela","flag-british-virgin-islands","flag-us-virgin-islands","flag-vietnam","flag-vanuatu","flag-wallis--futuna","flag-samoa","flag-kosovo","flag-yemen","flag-mayotte","flag-south-africa","flag-zambia","flag-zimbabwe","flag-england","flag-scotland","flag-wales"]}],"emojis":{"grinning-face":{"a":"Grinning Face","b":"1F600","j":["face","grin","smile","happy","joy",":D"]},"grinning-face-with-big-eyes":{"a":"Grinning Face with Big Eyes","b":"1F603","j":["face","mouth","open","smile","happy","joy","haha",":D",":)","funny"]},"grinning-face-with-smiling-eyes":{"a":"Grinning Face with Smiling Eyes","b":"1F604","j":["eye","face","mouth","open","smile","happy","joy","funny","haha","laugh","like",":D",":)"]},"beaming-face-with-smiling-eyes":{"a":"Beaming Face with Smiling Eyes","b":"1F601","j":["eye","face","grin","smile","happy","joy","kawaii"]},"grinning-squinting-face":{"a":"Grinning Squinting Face","b":"1F606","j":["face","laugh","mouth","satisfied","smile","happy","joy","lol","haha","glad","XD"]},"grinning-face-with-sweat":{"a":"Grinning Face with Sweat","b":"1F605","j":["cold","face","open","smile","sweat","hot","happy","laugh","relief"]},"rolling-on-the-floor-laughing":{"a":"Rolling on the Floor Laughing","b":"1F923","j":["face","floor","laugh","rofl","rolling","rotfl","laughing","lol","haha"]},"face-with-tears-of-joy":{"a":"Face with Tears of Joy","b":"1F602","j":["face","joy","laugh","tear","cry","tears","weep","happy","happytears","haha"]},"slightly-smiling-face":{"a":"Slightly Smiling Face","b":"1F642","j":["face","smile"]},"upsidedown-face":{"a":"Upside-Down Face","b":"1F643","j":["face","upside-down","upside_down_face","flipped","silly","smile"]},"melting-face":{"a":"Melting Face","b":"1FAE0","j":["disappear","dissolve","liquid","melt","hot","heat"]},"winking-face":{"a":"Winking Face","b":"1F609","j":["face","wink","happy","mischievous","secret",";)","smile","eye"]},"smiling-face-with-smiling-eyes":{"a":"Smiling Face with Smiling Eyes","b":"1F60A","j":["blush","eye","face","smile","happy","flushed","crush","embarrassed","shy","joy"]},"smiling-face-with-halo":{"a":"Smiling Face with Halo","b":"1F607","j":["angel","face","fantasy","halo","innocent","heaven"]},"smiling-face-with-hearts":{"a":"Smiling Face with Hearts","b":"1F970","j":["adore","crush","hearts","in love","face","love","like","affection","valentines","infatuation"]},"smiling-face-with-hearteyes":{"a":"Smiling Face with Heart-Eyes","b":"1F60D","j":["eye","face","love","smile","smiling face with heart-eyes","smiling_face_with_heart_eyes","like","affection","valentines","infatuation","crush","heart"]},"starstruck":{"a":"Star-Struck","b":"1F929","j":["eyes","face","grinning","star","star-struck","starry-eyed","star_struck","smile","starry"]},"face-blowing-a-kiss":{"a":"Face Blowing a Kiss","b":"1F618","j":["face","kiss","love","like","affection","valentines","infatuation"]},"kissing-face":{"a":"Kissing Face","b":"1F617","j":["face","kiss","love","like","3","valentines","infatuation"]},"smiling-face":{"a":"Smiling Face","b":"263A-FE0F","j":["face","outlined","relaxed","smile","blush","massage","happiness"]},"kissing-face-with-closed-eyes":{"a":"Kissing Face with Closed Eyes","b":"1F61A","j":["closed","eye","face","kiss","love","like","affection","valentines","infatuation"]},"kissing-face-with-smiling-eyes":{"a":"Kissing Face with Smiling Eyes","b":"1F619","j":["eye","face","kiss","smile","affection","valentines","infatuation"]},"smiling-face-with-tear":{"a":"Smiling Face with Tear","b":"1F972","j":["grateful","proud","relieved","smiling","tear","touched","sad","cry","pretend"]},"face-savoring-food":{"a":"Face Savoring Food","b":"1F60B","j":["delicious","face","savouring","smile","yum","happy","joy","tongue","silly","yummy","nom"]},"face-with-tongue":{"a":"Face with Tongue","b":"1F61B","j":["face","tongue","prank","childish","playful","mischievous","smile"]},"winking-face-with-tongue":{"a":"Winking Face with Tongue","b":"1F61C","j":["eye","face","joke","tongue","wink","prank","childish","playful","mischievous","smile"]},"zany-face":{"a":"Zany Face","b":"1F92A","j":["eye","goofy","large","small","face","crazy"]},"squinting-face-with-tongue":{"a":"Squinting Face with Tongue","b":"1F61D","j":["eye","face","horrible","taste","tongue","prank","playful","mischievous","smile"]},"moneymouth-face":{"a":"Money-Mouth Face","b":"1F911","j":["face","money","money-mouth face","mouth","money_mouth_face","rich","dollar"]},"smiling-face-with-open-hands":{"a":"Smiling Face with Open Hands","b":"1F917","j":["face","hug","hugging","open hands","smiling face","hugging_face","smile"]},"face-with-hand-over-mouth":{"a":"Face with Hand over Mouth","b":"1F92D","j":["whoops","shock","sudden realization","surprise","face"]},"face-with-open-eyes-and-hand-over-mouth":{"a":"Face with Open Eyes and Hand over Mouth","b":"1FAE2","j":["amazement","awe","disbelief","embarrass","scared","surprise","silence","secret","shock"]},"face-with-peeking-eye":{"a":"Face with Peeking Eye","b":"1FAE3","j":["captivated","peep","stare","scared","frightening","embarrassing","shy"]},"shushing-face":{"a":"Shushing Face","b":"1F92B","j":["quiet","shush","face","shhh"]},"thinking-face":{"a":"Thinking Face","b":"1F914","j":["face","thinking","hmmm","think","consider"]},"saluting-face":{"a":"Saluting Face","b":"1FAE1","j":["OK","salute","sunny","troops","yes","respect"]},"zippermouth-face":{"a":"Zipper-Mouth Face","b":"1F910","j":["face","mouth","zipper","zipper-mouth face","zipper_mouth_face","sealed","secret"]},"face-with-raised-eyebrow":{"a":"Face with Raised Eyebrow","b":"1F928","j":["distrust","skeptic","disapproval","disbelief","mild surprise","scepticism","face","surprise"]},"neutral-face":{"a":"Neutral Face","b":"1F610-FE0F","j":["deadpan","face","meh","neutral","indifference",":|"]},"expressionless-face":{"a":"Expressionless Face","b":"1F611","j":["expressionless","face","inexpressive","meh","unexpressive","indifferent","-_-","deadpan"]},"face-without-mouth":{"a":"Face Without Mouth","b":"1F636","j":["face","mouth","quiet","silent","hellokitty"]},"dotted-line-face":{"a":"Dotted Line Face","b":"1FAE5","j":["depressed","disappear","hide","introvert","invisible","lonely","isolation","depression"]},"face-in-clouds":{"a":"Face in Clouds","b":"1F636-200D-1F32B-FE0F","j":["absentminded","face in the fog","head in clouds","shower","steam","dream"]},"smirking-face":{"a":"Smirking Face","b":"1F60F","j":["face","smirk","smile","mean","prank","smug","sarcasm"]},"unamused-face":{"a":"Unamused Face","b":"1F612","j":["face","unamused","unhappy","indifference","bored","straight face","serious","sarcasm","unimpressed","skeptical","dubious","side_eye"]},"face-with-rolling-eyes":{"a":"Face with Rolling Eyes","b":"1F644","j":["eyeroll","eyes","face","rolling","frustrated"]},"grimacing-face":{"a":"Grimacing Face","b":"1F62C","j":["face","grimace","teeth"]},"face-exhaling":{"a":"Face Exhaling","b":"1F62E-200D-1F4A8","j":["exhale","gasp","groan","relief","whisper","whistle","relieve","tired","sigh"]},"lying-face":{"a":"Lying Face","b":"1F925","j":["face","lie","pinocchio"]},"shaking-face":{"a":"⊛ Shaking Face","b":"1FAE8","j":["earthquake","face","shaking","shock","vibrate","dizzy","blurry"]},"relieved-face":{"a":"Relieved Face","b":"1F60C","j":["face","relieved","relaxed","phew","massage","happiness"]},"pensive-face":{"a":"Pensive Face","b":"1F614","j":["dejected","face","pensive","sad","depressed","upset"]},"sleepy-face":{"a":"Sleepy Face","b":"1F62A","j":["face","good night","sleep","tired","rest","nap"]},"drooling-face":{"a":"Drooling Face","b":"1F924","j":["drooling","face"]},"sleeping-face":{"a":"Sleeping Face","b":"1F634","j":["face","good night","sleep","ZZZ","tired","sleepy","night","zzz"]},"face-with-medical-mask":{"a":"Face with Medical Mask","b":"1F637","j":["cold","doctor","face","mask","sick","ill","disease","covid"]},"face-with-thermometer":{"a":"Face with Thermometer","b":"1F912","j":["face","ill","sick","thermometer","temperature","cold","fever","covid"]},"face-with-headbandage":{"a":"Face with Head-Bandage","b":"1F915","j":["bandage","face","face with head-bandage","hurt","injury","face_with_head_bandage","injured","clumsy"]},"nauseated-face":{"a":"Nauseated Face","b":"1F922","j":["face","nauseated","vomit","gross","green","sick","throw up","ill"]},"face-vomiting":{"a":"Face Vomiting","b":"1F92E","j":["puke","sick","vomit","face"]},"sneezing-face":{"a":"Sneezing Face","b":"1F927","j":["face","gesundheit","sneeze","sick","allergy"]},"hot-face":{"a":"Hot Face","b":"1F975","j":["feverish","heat stroke","hot","red-faced","sweating","face","heat","red"]},"cold-face":{"a":"Cold Face","b":"1F976","j":["blue-faced","cold","freezing","frostbite","icicles","face","blue","frozen"]},"woozy-face":{"a":"Woozy Face","b":"1F974","j":["dizzy","intoxicated","tipsy","uneven eyes","wavy mouth","face","wavy"]},"face-with-crossedout-eyes":{"a":"Face with Crossed-out Eyes","b":"1F635","j":["crossed-out eyes","dead","face","face with crossed-out eyes","knocked out","dizzy_face","spent","unconscious","xox","dizzy"]},"face-with-spiral-eyes":{"a":"Face with Spiral Eyes","b":"1F635-200D-1F4AB","j":["dizzy","hypnotized","spiral","trouble","whoa","sick","ill","confused","nauseous","nausea"]},"exploding-head":{"a":"Exploding Head","b":"1F92F","j":["mind blown","shocked","face","mind","blown"]},"cowboy-hat-face":{"a":"Cowboy Hat Face","b":"1F920","j":["cowboy","cowgirl","face","hat"]},"partying-face":{"a":"Partying Face","b":"1F973","j":["celebration","hat","horn","party","face","woohoo"]},"disguised-face":{"a":"Disguised Face","b":"1F978","j":["disguise","face","glasses","incognito","nose","pretent","brows","moustache"]},"smiling-face-with-sunglasses":{"a":"Smiling Face with Sunglasses","b":"1F60E","j":["bright","cool","face","sun","sunglasses","smile","summer","beach","sunglass"]},"nerd-face":{"a":"Nerd Face","b":"1F913","j":["face","geek","nerd","nerdy","dork"]},"face-with-monocle":{"a":"Face with Monocle","b":"1F9D0","j":["face","monocle","stuffy","wealthy"]},"confused-face":{"a":"Confused Face","b":"1F615","j":["confused","face","meh","indifference","huh","weird","hmmm",":/"]},"face-with-diagonal-mouth":{"a":"Face with Diagonal Mouth","b":"1FAE4","j":["disappointed","meh","skeptical","unsure","skeptic","confuse","frustrated","indifferent"]},"worried-face":{"a":"Worried Face","b":"1F61F","j":["face","worried","concern","nervous",":("]},"slightly-frowning-face":{"a":"Slightly Frowning Face","b":"1F641","j":["face","frown","frowning","disappointed","sad","upset"]},"frowning-face":{"a":"Frowning Face","b":"2639-FE0F","j":["face","frown","sad","upset"]},"face-with-open-mouth":{"a":"Face with Open Mouth","b":"1F62E","j":["face","mouth","open","sympathy","surprise","impressed","wow","whoa",":O"]},"hushed-face":{"a":"Hushed Face","b":"1F62F","j":["face","hushed","stunned","surprised","woo","shh"]},"astonished-face":{"a":"Astonished Face","b":"1F632","j":["astonished","face","shocked","totally","xox","surprised","poisoned"]},"flushed-face":{"a":"Flushed Face","b":"1F633","j":["dazed","face","flushed","blush","shy","flattered"]},"pleading-face":{"a":"Pleading Face","b":"1F97A","j":["begging","mercy","puppy eyes","face","cry","tears","sad","grievance"]},"face-holding-back-tears":{"a":"Face Holding Back Tears","b":"1F979","j":["angry","cry","proud","resist","sad","touched","gratitude"]},"frowning-face-with-open-mouth":{"a":"Frowning Face with Open Mouth","b":"1F626","j":["face","frown","mouth","open","aw","what"]},"anguished-face":{"a":"Anguished Face","b":"1F627","j":["anguished","face","stunned","nervous"]},"fearful-face":{"a":"Fearful Face","b":"1F628","j":["face","fear","fearful","scared","terrified","nervous"]},"anxious-face-with-sweat":{"a":"Anxious Face with Sweat","b":"1F630","j":["blue","cold","face","rushed","sweat","nervous"]},"sad-but-relieved-face":{"a":"Sad but Relieved Face","b":"1F625","j":["disappointed","face","relieved","whew","phew","sweat","nervous"]},"crying-face":{"a":"Crying Face","b":"1F622","j":["cry","face","sad","tear","tears","depressed","upset",":'("]},"loudly-crying-face":{"a":"Loudly Crying Face","b":"1F62D","j":["cry","face","sad","sob","tear","tears","upset","depressed"]},"face-screaming-in-fear":{"a":"Face Screaming in Fear","b":"1F631","j":["face","fear","munch","scared","scream","omg"]},"confounded-face":{"a":"Confounded Face","b":"1F616","j":["confounded","face","confused","sick","unwell","oops",":S"]},"persevering-face":{"a":"Persevering Face","b":"1F623","j":["face","persevere","sick","no","upset","oops"]},"disappointed-face":{"a":"Disappointed Face","b":"1F61E","j":["disappointed","face","sad","upset","depressed",":("]},"downcast-face-with-sweat":{"a":"Downcast Face with Sweat","b":"1F613","j":["cold","face","sweat","hot","sad","tired","exercise"]},"weary-face":{"a":"Weary Face","b":"1F629","j":["face","tired","weary","sleepy","sad","frustrated","upset"]},"tired-face":{"a":"Tired Face","b":"1F62B","j":["face","tired","sick","whine","upset","frustrated"]},"yawning-face":{"a":"Yawning Face","b":"1F971","j":["bored","tired","yawn","sleepy"]},"face-with-steam-from-nose":{"a":"Face with Steam From Nose","b":"1F624","j":["face","triumph","won","gas","phew","proud","pride"]},"enraged-face":{"a":"Enraged Face","b":"1F621","j":["angry","enraged","face","mad","pouting","rage","red","pouting_face","hate","despise"]},"angry-face":{"a":"Angry Face","b":"1F620","j":["anger","angry","face","mad","annoyed","frustrated"]},"face-with-symbols-on-mouth":{"a":"Face with Symbols on Mouth","b":"1F92C","j":["swearing","cursing","face","cussing","profanity","expletive"]},"smiling-face-with-horns":{"a":"Smiling Face with Horns","b":"1F608","j":["face","fairy tale","fantasy","horns","smile","devil"]},"angry-face-with-horns":{"a":"Angry Face with Horns","b":"1F47F","j":["demon","devil","face","fantasy","imp","angry","horns"]},"skull":{"a":"Skull","b":"1F480","j":["death","face","fairy tale","monster","dead","skeleton","creepy"]},"skull-and-crossbones":{"a":"Skull and Crossbones","b":"2620-FE0F","j":["crossbones","death","face","monster","skull","poison","danger","deadly","scary","pirate","evil"]},"pile-of-poo":{"a":"Pile of Poo","b":"1F4A9","j":["dung","face","monster","poo","poop","hankey","shitface","fail","turd","shit"]},"clown-face":{"a":"Clown Face","b":"1F921","j":["clown","face"]},"ogre":{"a":"Ogre","b":"1F479","j":["creature","face","fairy tale","fantasy","monster","troll","red","mask","halloween","scary","creepy","devil","demon","japanese_ogre"]},"goblin":{"a":"Goblin","b":"1F47A","j":["creature","face","fairy tale","fantasy","monster","red","evil","mask","scary","creepy","japanese_goblin"]},"ghost":{"a":"Ghost","b":"1F47B","j":["creature","face","fairy tale","fantasy","monster","halloween","spooky","scary"]},"alien":{"a":"Alien","b":"1F47D-FE0F","j":["creature","extraterrestrial","face","fantasy","ufo","UFO","paul","weird","outer_space"]},"alien-monster":{"a":"Alien Monster","b":"1F47E","j":["alien","creature","extraterrestrial","face","monster","ufo","game","arcade","play"]},"robot":{"a":"Robot","b":"1F916","j":["face","monster","computer","machine","bot"]},"grinning-cat":{"a":"Grinning Cat","b":"1F63A","j":["cat","face","grinning","mouth","open","smile","animal","cats","happy"]},"grinning-cat-with-smiling-eyes":{"a":"Grinning Cat with Smiling Eyes","b":"1F638","j":["cat","eye","face","grin","smile","animal","cats"]},"cat-with-tears-of-joy":{"a":"Cat with Tears of Joy","b":"1F639","j":["cat","face","joy","tear","animal","cats","haha","happy","tears"]},"smiling-cat-with-hearteyes":{"a":"Smiling Cat with Heart-Eyes","b":"1F63B","j":["cat","eye","face","heart","love","smile","smiling cat with heart-eyes","smiling_cat_with_heart_eyes","animal","like","affection","cats","valentines"]},"cat-with-wry-smile":{"a":"Cat with Wry Smile","b":"1F63C","j":["cat","face","ironic","smile","wry","animal","cats","smirk"]},"kissing-cat":{"a":"Kissing Cat","b":"1F63D","j":["cat","eye","face","kiss","animal","cats"]},"weary-cat":{"a":"Weary Cat","b":"1F640","j":["cat","face","oh","surprised","weary","animal","cats","munch","scared","scream"]},"crying-cat":{"a":"Crying Cat","b":"1F63F","j":["cat","cry","face","sad","tear","animal","tears","weep","cats","upset"]},"pouting-cat":{"a":"Pouting Cat","b":"1F63E","j":["cat","face","pouting","animal","cats"]},"seenoevil-monkey":{"a":"See-No-Evil Monkey","b":"1F648","j":["evil","face","forbidden","monkey","see","see-no-evil monkey","see_no_evil_monkey","animal","nature","haha"]},"hearnoevil-monkey":{"a":"Hear-No-Evil Monkey","b":"1F649","j":["evil","face","forbidden","hear","hear-no-evil monkey","monkey","hear_no_evil_monkey","animal","nature"]},"speaknoevil-monkey":{"a":"Speak-No-Evil Monkey","b":"1F64A","j":["evil","face","forbidden","monkey","speak","speak-no-evil monkey","speak_no_evil_monkey","animal","nature","omg"]},"love-letter":{"a":"Love Letter","b":"1F48C","j":["heart","letter","love","mail","email","like","affection","envelope","valentines"]},"heart-with-arrow":{"a":"Heart with Arrow","b":"1F498","j":["arrow","cupid","love","like","heart","affection","valentines"]},"heart-with-ribbon":{"a":"Heart with Ribbon","b":"1F49D","j":["ribbon","valentine","love","valentines"]},"sparkling-heart":{"a":"Sparkling Heart","b":"1F496","j":["excited","sparkle","love","like","affection","valentines"]},"growing-heart":{"a":"Growing Heart","b":"1F497","j":["excited","growing","nervous","pulse","like","love","affection","valentines","pink"]},"beating-heart":{"a":"Beating Heart","b":"1F493","j":["beating","heartbeat","pulsating","love","like","affection","valentines","pink","heart"]},"revolving-hearts":{"a":"Revolving Hearts","b":"1F49E","j":["revolving","love","like","affection","valentines"]},"two-hearts":{"a":"Two Hearts","b":"1F495","j":["love","like","affection","valentines","heart"]},"heart-decoration":{"a":"Heart Decoration","b":"1F49F","j":["heart","purple-square","love","like"]},"heart-exclamation":{"a":"Heart Exclamation","b":"2763-FE0F","j":["exclamation","mark","punctuation","decoration","love"]},"broken-heart":{"a":"Broken Heart","b":"1F494","j":["break","broken","sad","sorry","heart","heartbreak"]},"heart-on-fire":{"a":"Heart on Fire","b":"2764-FE0F-200D-1F525","j":["burn","heart","love","lust","sacred heart","passionate","enthusiastic"]},"mending-heart":{"a":"Mending Heart","b":"2764-FE0F-200D-1FA79","j":["healthier","improving","mending","recovering","recuperating","well","broken heart","bandage","wounded"]},"red-heart":{"a":"Red Heart","b":"2764-FE0F","j":["heart","love","like","valentines"]},"pink-heart":{"a":"⊛ Pink Heart","b":"1FA77","j":["cute","heart","like","love","pink","valentines"]},"orange-heart":{"a":"Orange Heart","b":"1F9E1","j":["orange","love","like","affection","valentines"]},"yellow-heart":{"a":"Yellow Heart","b":"1F49B","j":["yellow","love","like","affection","valentines"]},"green-heart":{"a":"Green Heart","b":"1F49A","j":["green","love","like","affection","valentines"]},"blue-heart":{"a":"Blue Heart","b":"1F499","j":["blue","love","like","affection","valentines"]},"light-blue-heart":{"a":"⊛ Light Blue Heart","b":"1FA75","j":["cyan","heart","light blue","teal","ice","baby blue"]},"purple-heart":{"a":"Purple Heart","b":"1F49C","j":["purple","love","like","affection","valentines"]},"brown-heart":{"a":"Brown Heart","b":"1F90E","j":["brown","heart","coffee"]},"black-heart":{"a":"Black Heart","b":"1F5A4","j":["black","evil","wicked"]},"grey-heart":{"a":"⊛ Grey Heart","b":"1FA76","j":["gray","heart","silver","slate","monochrome"]},"white-heart":{"a":"White Heart","b":"1F90D","j":["heart","white","pure"]},"kiss-mark":{"a":"Kiss Mark","b":"1F48B","j":["kiss","lips","face","love","like","affection","valentines"]},"hundred-points":{"a":"Hundred Points","b":"1F4AF","j":["100","full","hundred","score","perfect","numbers","century","exam","quiz","test","pass"]},"anger-symbol":{"a":"Anger Symbol","b":"1F4A2","j":["angry","comic","mad"]},"collision":{"a":"Collision","b":"1F4A5","j":["boom","comic","bomb","explode","explosion","blown"]},"dizzy":{"a":"Dizzy","b":"1F4AB","j":["comic","star","sparkle","shoot","magic"]},"sweat-droplets":{"a":"Sweat Droplets","b":"1F4A6","j":["comic","splashing","sweat","water","drip","oops"]},"dashing-away":{"a":"Dashing Away","b":"1F4A8","j":["comic","dash","running","wind","air","fast","shoo","fart","smoke","puff"]},"hole":{"a":"Hole","b":"1F573-FE0F","j":["embarrassing"]},"speech-balloon":{"a":"Speech Balloon","b":"1F4AC","j":["balloon","bubble","comic","dialog","speech","words","message","talk","chatting"]},"eye-in-speech-bubble":{"a":"Eye in Speech Bubble","b":"1F441-FE0F-200D-1F5E8-FE0F","j":["balloon","bubble","eye","speech","witness","info"]},"left-speech-bubble":{"a":"Left Speech Bubble","b":"1F5E8-FE0F","j":["balloon","bubble","dialog","speech","words","message","talk","chatting"]},"right-anger-bubble":{"a":"Right Anger Bubble","b":"1F5EF-FE0F","j":["angry","balloon","bubble","mad","caption","speech","thinking"]},"thought-balloon":{"a":"Thought Balloon","b":"1F4AD","j":["balloon","bubble","comic","thought","cloud","speech","thinking","dream"]},"zzz":{"a":"Zzz","b":"1F4A4","j":["comic","good night","sleep","ZZZ","sleepy","tired","dream"]},"waving-hand":{"a":"Waving Hand","b":"1F44B","j":["hand","wave","waving","hands","gesture","goodbye","solong","farewell","hello","hi","palm"]},"raised-back-of-hand":{"a":"Raised Back of Hand","b":"1F91A","j":["backhand","raised","fingers"]},"hand-with-fingers-splayed":{"a":"Hand with Fingers Splayed","b":"1F590-FE0F","j":["finger","hand","splayed","fingers","palm"]},"raised-hand":{"a":"Raised Hand","b":"270B","j":["hand","high 5","high five","fingers","stop","highfive","palm","ban"]},"vulcan-salute":{"a":"Vulcan Salute","b":"1F596","j":["finger","hand","spock","vulcan","fingers","star trek"]},"rightwards-hand":{"a":"Rightwards Hand","b":"1FAF1","j":["hand","right","rightward","palm","offer"]},"leftwards-hand":{"a":"Leftwards Hand","b":"1FAF2","j":["hand","left","leftward","palm","offer"]},"palm-down-hand":{"a":"Palm Down Hand","b":"1FAF3","j":["dismiss","drop","shoo","palm"]},"palm-up-hand":{"a":"Palm Up Hand","b":"1FAF4","j":["beckon","catch","come","offer","lift","demand"]},"leftwards-pushing-hand":{"a":"⊛ Leftwards Pushing Hand","b":"1FAF7","j":["high five","leftward","push","refuse","stop","wait","highfive","pressing"]},"rightwards-pushing-hand":{"a":"⊛ Rightwards Pushing Hand","b":"1FAF8","j":["high five","push","refuse","rightward","stop","wait","highfive","pressing"]},"ok-hand":{"a":"Ok Hand","b":"1F44C","j":["hand","OK","fingers","limbs","perfect","ok","okay"]},"pinched-fingers":{"a":"Pinched Fingers","b":"1F90C","j":["fingers","hand gesture","interrogation","pinched","sarcastic","size","tiny","small"]},"pinching-hand":{"a":"Pinching Hand","b":"1F90F","j":["small amount","tiny","small","size"]},"victory-hand":{"a":"Victory Hand","b":"270C-FE0F","j":["hand","v","victory","fingers","ohyeah","peace","two"]},"crossed-fingers":{"a":"Crossed Fingers","b":"1F91E","j":["cross","finger","hand","luck","good","lucky"]},"hand-with-index-finger-and-thumb-crossed":{"a":"Hand with Index Finger and Thumb Crossed","b":"1FAF0","j":["expensive","heart","love","money","snap"]},"loveyou-gesture":{"a":"Love-You Gesture","b":"1F91F","j":["hand","ILY","love-you gesture","love_you_gesture","fingers","gesture"]},"sign-of-the-horns":{"a":"Sign of the Horns","b":"1F918","j":["finger","hand","horns","rock-on","fingers","evil_eye","sign_of_horns","rock_on"]},"call-me-hand":{"a":"Call Me Hand","b":"1F919","j":["call","hand","hang loose","Shaka","hands","gesture","shaka"]},"backhand-index-pointing-left":{"a":"Backhand Index Pointing Left","b":"1F448-FE0F","j":["backhand","finger","hand","index","point","direction","fingers","left"]},"backhand-index-pointing-right":{"a":"Backhand Index Pointing Right","b":"1F449-FE0F","j":["backhand","finger","hand","index","point","fingers","direction","right"]},"backhand-index-pointing-up":{"a":"Backhand Index Pointing Up","b":"1F446-FE0F","j":["backhand","finger","hand","point","up","fingers","direction"]},"middle-finger":{"a":"Middle Finger","b":"1F595","j":["finger","hand","fingers","rude","middle","flipping"]},"backhand-index-pointing-down":{"a":"Backhand Index Pointing Down","b":"1F447-FE0F","j":["backhand","down","finger","hand","point","fingers","direction"]},"index-pointing-up":{"a":"Index Pointing Up","b":"261D-FE0F","j":["finger","hand","index","point","up","fingers","direction"]},"index-pointing-at-the-viewer":{"a":"Index Pointing at the Viewer","b":"1FAF5","j":["point","you","recruit"]},"thumbs-up":{"a":"Thumbs Up","b":"1F44D-FE0F","j":["+1","hand","thumb","up","thumbsup","yes","awesome","good","agree","accept","cool","like"]},"thumbs-down":{"a":"Thumbs Down","b":"1F44E-FE0F","j":["-1","down","hand","thumb","thumbsdown","no","dislike"]},"raised-fist":{"a":"Raised Fist","b":"270A","j":["clenched","fist","hand","punch","fingers","grasp"]},"oncoming-fist":{"a":"Oncoming Fist","b":"1F44A","j":["clenched","fist","hand","punch","angry","violence","hit","attack"]},"leftfacing-fist":{"a":"Left-Facing Fist","b":"1F91B","j":["fist","left-facing fist","leftwards","left_facing_fist","hand","fistbump"]},"rightfacing-fist":{"a":"Right-Facing Fist","b":"1F91C","j":["fist","right-facing fist","rightwards","right_facing_fist","hand","fistbump"]},"clapping-hands":{"a":"Clapping Hands","b":"1F44F","j":["clap","hand","hands","praise","applause","congrats","yay"]},"raising-hands":{"a":"Raising Hands","b":"1F64C","j":["celebration","gesture","hand","hooray","raised","yea","hands"]},"heart-hands":{"a":"Heart Hands","b":"1FAF6","j":["love","appreciation","support"]},"open-hands":{"a":"Open Hands","b":"1F450","j":["hand","open","fingers","butterfly","hands"]},"palms-up-together":{"a":"Palms Up Together","b":"1F932","j":["prayer","cupped hands","hands","gesture","cupped"]},"handshake":{"a":"Handshake","b":"1F91D","j":["agreement","hand","meeting","shake"]},"folded-hands":{"a":"Folded Hands","b":"1F64F","j":["ask","hand","high 5","high five","please","pray","thanks","hope","wish","namaste","highfive","thank you","appreciate"]},"writing-hand":{"a":"Writing Hand","b":"270D-FE0F","j":["hand","write","lower_left_ballpoint_pen","stationery","compose"]},"nail-polish":{"a":"Nail Polish","b":"1F485","j":["care","cosmetics","manicure","nail","polish","beauty","finger","fashion"]},"selfie":{"a":"Selfie","b":"1F933","j":["camera","phone"]},"flexed-biceps":{"a":"Flexed Biceps","b":"1F4AA","j":["biceps","comic","flex","muscle","arm","hand","summer","strong"]},"mechanical-arm":{"a":"Mechanical Arm","b":"1F9BE","j":["accessibility","prosthetic"]},"mechanical-leg":{"a":"Mechanical Leg","b":"1F9BF","j":["accessibility","prosthetic"]},"leg":{"a":"Leg","b":"1F9B5","j":["kick","limb"]},"foot":{"a":"Foot","b":"1F9B6","j":["kick","stomp"]},"ear":{"a":"Ear","b":"1F442-FE0F","j":["body","face","hear","sound","listen"]},"ear-with-hearing-aid":{"a":"Ear with Hearing Aid","b":"1F9BB","j":["accessibility","hard of hearing"]},"nose":{"a":"Nose","b":"1F443","j":["body","smell","sniff"]},"brain":{"a":"Brain","b":"1F9E0","j":["intelligent","smart"]},"anatomical-heart":{"a":"Anatomical Heart","b":"1FAC0","j":["anatomical","cardiology","heart","organ","pulse","health","heartbeat"]},"lungs":{"a":"Lungs","b":"1FAC1","j":["breath","exhalation","inhalation","organ","respiration","breathe"]},"tooth":{"a":"Tooth","b":"1F9B7","j":["dentist","teeth"]},"bone":{"a":"Bone","b":"1F9B4","j":["skeleton"]},"eyes":{"a":"Eyes","b":"1F440","j":["eye","face","look","watch","stalk","peek","see"]},"eye":{"a":"Eye","b":"1F441-FE0F","j":["body","face","look","see","watch","stare"]},"tongue":{"a":"Tongue","b":"1F445","j":["body","mouth","playful"]},"mouth":{"a":"Mouth","b":"1F444","j":["lips","kiss"]},"biting-lip":{"a":"Biting Lip","b":"1FAE6","j":["anxious","fear","flirting","nervous","uncomfortable","worried","flirt","sexy","pain","worry"]},"baby":{"a":"Baby","b":"1F476","j":["young","child","boy","girl","toddler"]},"child":{"a":"Child","b":"1F9D2","j":["gender-neutral","unspecified gender","young"]},"boy":{"a":"Boy","b":"1F466","j":["young","man","male","guy","teenager"]},"girl":{"a":"Girl","b":"1F467","j":["Virgo","young","zodiac","female","woman","teenager"]},"person":{"a":"Person","b":"1F9D1","j":["adult","gender-neutral","unspecified gender"]},"person-blond-hair":{"a":"Person: Blond Hair","b":"1F471","j":["blond","blond-haired person","hair","person: blond hair","hairstyle"]},"man":{"a":"Man","b":"1F468","j":["adult","mustache","father","dad","guy","classy","sir","moustache"]},"person-beard":{"a":"Person: Beard","b":"1F9D4","j":["beard","person","person: beard","bewhiskered","man_beard"]},"man-beard":{"a":"Man: Beard","b":"1F9D4-200D-2642-FE0F","j":["beard","man","man: beard","facial hair"]},"woman-beard":{"a":"Woman: Beard","b":"1F9D4-200D-2640-FE0F","j":["beard","woman","woman: beard","facial hair"]},"man-red-hair":{"a":"Man: Red Hair","b":"1F468-200D-1F9B0","j":["adult","man","red hair","hairstyle"]},"man-curly-hair":{"a":"Man: Curly Hair","b":"1F468-200D-1F9B1","j":["adult","curly hair","man","hairstyle"]},"man-white-hair":{"a":"Man: White Hair","b":"1F468-200D-1F9B3","j":["adult","man","white hair","old","elder"]},"man-bald":{"a":"Man: Bald","b":"1F468-200D-1F9B2","j":["adult","bald","man","hairless"]},"woman":{"a":"Woman","b":"1F469","j":["adult","female","girls","lady"]},"woman-red-hair":{"a":"Woman: Red Hair","b":"1F469-200D-1F9B0","j":["adult","red hair","woman","hairstyle"]},"person-red-hair":{"a":"Person: Red Hair","b":"1F9D1-200D-1F9B0","j":["adult","gender-neutral","person","red hair","unspecified gender","hairstyle"]},"woman-curly-hair":{"a":"Woman: Curly Hair","b":"1F469-200D-1F9B1","j":["adult","curly hair","woman","hairstyle"]},"person-curly-hair":{"a":"Person: Curly Hair","b":"1F9D1-200D-1F9B1","j":["adult","curly hair","gender-neutral","person","unspecified gender","hairstyle"]},"woman-white-hair":{"a":"Woman: White Hair","b":"1F469-200D-1F9B3","j":["adult","white hair","woman","old","elder"]},"person-white-hair":{"a":"Person: White Hair","b":"1F9D1-200D-1F9B3","j":["adult","gender-neutral","person","unspecified gender","white hair","elder","old"]},"woman-bald":{"a":"Woman: Bald","b":"1F469-200D-1F9B2","j":["adult","bald","woman","hairless"]},"person-bald":{"a":"Person: Bald","b":"1F9D1-200D-1F9B2","j":["adult","bald","gender-neutral","person","unspecified gender","hairless"]},"woman-blond-hair":{"a":"Woman: Blond Hair","b":"1F471-200D-2640-FE0F","j":["blond-haired woman","blonde","hair","woman","woman: blond hair","female","girl","person"]},"man-blond-hair":{"a":"Man: Blond Hair","b":"1F471-200D-2642-FE0F","j":["blond","blond-haired man","hair","man","man: blond hair","male","boy","blonde","guy","person"]},"older-person":{"a":"Older Person","b":"1F9D3","j":["adult","gender-neutral","old","unspecified gender","human","elder","senior"]},"old-man":{"a":"Old Man","b":"1F474","j":["adult","man","old","human","male","men","elder","senior"]},"old-woman":{"a":"Old Woman","b":"1F475","j":["adult","old","woman","human","female","women","lady","elder","senior"]},"person-frowning":{"a":"Person Frowning","b":"1F64D","j":["frown","gesture","worried"]},"man-frowning":{"a":"Man Frowning","b":"1F64D-200D-2642-FE0F","j":["frowning","gesture","man","male","boy","sad","depressed","discouraged","unhappy"]},"woman-frowning":{"a":"Woman Frowning","b":"1F64D-200D-2640-FE0F","j":["frowning","gesture","woman","female","girl","sad","depressed","discouraged","unhappy"]},"person-pouting":{"a":"Person Pouting","b":"1F64E","j":["gesture","pouting","upset"]},"man-pouting":{"a":"Man Pouting","b":"1F64E-200D-2642-FE0F","j":["gesture","man","pouting","male","boy"]},"woman-pouting":{"a":"Woman Pouting","b":"1F64E-200D-2640-FE0F","j":["gesture","pouting","woman","female","girl"]},"person-gesturing-no":{"a":"Person Gesturing No","b":"1F645","j":["forbidden","gesture","hand","person gesturing NO","prohibited","decline"]},"man-gesturing-no":{"a":"Man Gesturing No","b":"1F645-200D-2642-FE0F","j":["forbidden","gesture","hand","man","man gesturing NO","prohibited","male","boy","nope"]},"woman-gesturing-no":{"a":"Woman Gesturing No","b":"1F645-200D-2640-FE0F","j":["forbidden","gesture","hand","prohibited","woman","woman gesturing NO","female","girl","nope"]},"person-gesturing-ok":{"a":"Person Gesturing Ok","b":"1F646","j":["gesture","hand","OK","person gesturing OK","agree"]},"man-gesturing-ok":{"a":"Man Gesturing Ok","b":"1F646-200D-2642-FE0F","j":["gesture","hand","man","man gesturing OK","OK","men","boy","male","blue","human"]},"woman-gesturing-ok":{"a":"Woman Gesturing Ok","b":"1F646-200D-2640-FE0F","j":["gesture","hand","OK","woman","woman gesturing OK","women","girl","female","pink","human"]},"person-tipping-hand":{"a":"Person Tipping Hand","b":"1F481","j":["hand","help","information","sassy","tipping"]},"man-tipping-hand":{"a":"Man Tipping Hand","b":"1F481-200D-2642-FE0F","j":["man","sassy","tipping hand","male","boy","human","information"]},"woman-tipping-hand":{"a":"Woman Tipping Hand","b":"1F481-200D-2640-FE0F","j":["sassy","tipping hand","woman","female","girl","human","information"]},"person-raising-hand":{"a":"Person Raising Hand","b":"1F64B","j":["gesture","hand","happy","raised","question"]},"man-raising-hand":{"a":"Man Raising Hand","b":"1F64B-200D-2642-FE0F","j":["gesture","man","raising hand","male","boy"]},"woman-raising-hand":{"a":"Woman Raising Hand","b":"1F64B-200D-2640-FE0F","j":["gesture","raising hand","woman","female","girl"]},"deaf-person":{"a":"Deaf Person","b":"1F9CF","j":["accessibility","deaf","ear","hear"]},"deaf-man":{"a":"Deaf Man","b":"1F9CF-200D-2642-FE0F","j":["deaf","man","accessibility"]},"deaf-woman":{"a":"Deaf Woman","b":"1F9CF-200D-2640-FE0F","j":["deaf","woman","accessibility"]},"person-bowing":{"a":"Person Bowing","b":"1F647","j":["apology","bow","gesture","sorry","respectiful"]},"man-bowing":{"a":"Man Bowing","b":"1F647-200D-2642-FE0F","j":["apology","bowing","favor","gesture","man","sorry","male","boy"]},"woman-bowing":{"a":"Woman Bowing","b":"1F647-200D-2640-FE0F","j":["apology","bowing","favor","gesture","sorry","woman","female","girl"]},"person-facepalming":{"a":"Person Facepalming","b":"1F926","j":["disbelief","exasperation","face","palm","disappointed"]},"man-facepalming":{"a":"Man Facepalming","b":"1F926-200D-2642-FE0F","j":["disbelief","exasperation","facepalm","man","male","boy"]},"woman-facepalming":{"a":"Woman Facepalming","b":"1F926-200D-2640-FE0F","j":["disbelief","exasperation","facepalm","woman","female","girl"]},"person-shrugging":{"a":"Person Shrugging","b":"1F937","j":["doubt","ignorance","indifference","shrug","regardless"]},"man-shrugging":{"a":"Man Shrugging","b":"1F937-200D-2642-FE0F","j":["doubt","ignorance","indifference","man","shrug","male","boy","confused","indifferent"]},"woman-shrugging":{"a":"Woman Shrugging","b":"1F937-200D-2640-FE0F","j":["doubt","ignorance","indifference","shrug","woman","female","girl","confused","indifferent"]},"health-worker":{"a":"Health Worker","b":"1F9D1-200D-2695-FE0F","j":["doctor","healthcare","nurse","therapist","hospital"]},"man-health-worker":{"a":"Man Health Worker","b":"1F468-200D-2695-FE0F","j":["doctor","healthcare","man","nurse","therapist","human"]},"woman-health-worker":{"a":"Woman Health Worker","b":"1F469-200D-2695-FE0F","j":["doctor","healthcare","nurse","therapist","woman","human"]},"student":{"a":"Student","b":"1F9D1-200D-1F393","j":["graduate","learn"]},"man-student":{"a":"Man Student","b":"1F468-200D-1F393","j":["graduate","man","student","human"]},"woman-student":{"a":"Woman Student","b":"1F469-200D-1F393","j":["graduate","student","woman","human"]},"teacher":{"a":"Teacher","b":"1F9D1-200D-1F3EB","j":["instructor","professor"]},"man-teacher":{"a":"Man Teacher","b":"1F468-200D-1F3EB","j":["instructor","man","professor","teacher","human"]},"woman-teacher":{"a":"Woman Teacher","b":"1F469-200D-1F3EB","j":["instructor","professor","teacher","woman","human"]},"judge":{"a":"Judge","b":"1F9D1-200D-2696-FE0F","j":["justice","scales","law"]},"man-judge":{"a":"Man Judge","b":"1F468-200D-2696-FE0F","j":["judge","justice","man","scales","court","human"]},"woman-judge":{"a":"Woman Judge","b":"1F469-200D-2696-FE0F","j":["judge","justice","scales","woman","court","human"]},"farmer":{"a":"Farmer","b":"1F9D1-200D-1F33E","j":["gardener","rancher","crops"]},"man-farmer":{"a":"Man Farmer","b":"1F468-200D-1F33E","j":["farmer","gardener","man","rancher","human"]},"woman-farmer":{"a":"Woman Farmer","b":"1F469-200D-1F33E","j":["farmer","gardener","rancher","woman","human"]},"cook":{"a":"Cook","b":"1F9D1-200D-1F373","j":["chef","food","kitchen","culinary"]},"man-cook":{"a":"Man Cook","b":"1F468-200D-1F373","j":["chef","cook","man","human"]},"woman-cook":{"a":"Woman Cook","b":"1F469-200D-1F373","j":["chef","cook","woman","human"]},"mechanic":{"a":"Mechanic","b":"1F9D1-200D-1F527","j":["electrician","plumber","tradesperson","worker","technician"]},"man-mechanic":{"a":"Man Mechanic","b":"1F468-200D-1F527","j":["electrician","man","mechanic","plumber","tradesperson","human","wrench"]},"woman-mechanic":{"a":"Woman Mechanic","b":"1F469-200D-1F527","j":["electrician","mechanic","plumber","tradesperson","woman","human","wrench"]},"factory-worker":{"a":"Factory Worker","b":"1F9D1-200D-1F3ED","j":["assembly","factory","industrial","worker","labor"]},"man-factory-worker":{"a":"Man Factory Worker","b":"1F468-200D-1F3ED","j":["assembly","factory","industrial","man","worker","human"]},"woman-factory-worker":{"a":"Woman Factory Worker","b":"1F469-200D-1F3ED","j":["assembly","factory","industrial","woman","worker","human"]},"office-worker":{"a":"Office Worker","b":"1F9D1-200D-1F4BC","j":["architect","business","manager","white-collar"]},"man-office-worker":{"a":"Man Office Worker","b":"1F468-200D-1F4BC","j":["architect","business","man","manager","white-collar","human"]},"woman-office-worker":{"a":"Woman Office Worker","b":"1F469-200D-1F4BC","j":["architect","business","manager","white-collar","woman","human"]},"scientist":{"a":"Scientist","b":"1F9D1-200D-1F52C","j":["biologist","chemist","engineer","physicist","chemistry"]},"man-scientist":{"a":"Man Scientist","b":"1F468-200D-1F52C","j":["biologist","chemist","engineer","man","physicist","scientist","human"]},"woman-scientist":{"a":"Woman Scientist","b":"1F469-200D-1F52C","j":["biologist","chemist","engineer","physicist","scientist","woman","human"]},"technologist":{"a":"Technologist","b":"1F9D1-200D-1F4BB","j":["coder","developer","inventor","software","computer"]},"man-technologist":{"a":"Man Technologist","b":"1F468-200D-1F4BB","j":["coder","developer","inventor","man","software","technologist","engineer","programmer","human","laptop","computer"]},"woman-technologist":{"a":"Woman Technologist","b":"1F469-200D-1F4BB","j":["coder","developer","inventor","software","technologist","woman","engineer","programmer","human","laptop","computer"]},"singer":{"a":"Singer","b":"1F9D1-200D-1F3A4","j":["actor","entertainer","rock","star","song","artist","performer"]},"man-singer":{"a":"Man Singer","b":"1F468-200D-1F3A4","j":["actor","entertainer","man","rock","singer","star","rockstar","human"]},"woman-singer":{"a":"Woman Singer","b":"1F469-200D-1F3A4","j":["actor","entertainer","rock","singer","star","woman","rockstar","human"]},"artist":{"a":"Artist","b":"1F9D1-200D-1F3A8","j":["palette","painting","draw","creativity"]},"man-artist":{"a":"Man Artist","b":"1F468-200D-1F3A8","j":["artist","man","palette","painter","human"]},"woman-artist":{"a":"Woman Artist","b":"1F469-200D-1F3A8","j":["artist","palette","woman","painter","human"]},"pilot":{"a":"Pilot","b":"1F9D1-200D-2708-FE0F","j":["plane","fly","airplane"]},"man-pilot":{"a":"Man Pilot","b":"1F468-200D-2708-FE0F","j":["man","pilot","plane","aviator","human"]},"woman-pilot":{"a":"Woman Pilot","b":"1F469-200D-2708-FE0F","j":["pilot","plane","woman","aviator","human"]},"astronaut":{"a":"Astronaut","b":"1F9D1-200D-1F680","j":["rocket","outerspace"]},"man-astronaut":{"a":"Man Astronaut","b":"1F468-200D-1F680","j":["astronaut","man","rocket","space","human"]},"woman-astronaut":{"a":"Woman Astronaut","b":"1F469-200D-1F680","j":["astronaut","rocket","woman","space","human"]},"firefighter":{"a":"Firefighter","b":"1F9D1-200D-1F692","j":["firetruck","fire"]},"man-firefighter":{"a":"Man Firefighter","b":"1F468-200D-1F692","j":["firefighter","firetruck","man","fireman","human"]},"woman-firefighter":{"a":"Woman Firefighter","b":"1F469-200D-1F692","j":["firefighter","firetruck","woman","fireman","human"]},"police-officer":{"a":"Police Officer","b":"1F46E","j":["cop","officer","police"]},"man-police-officer":{"a":"Man Police Officer","b":"1F46E-200D-2642-FE0F","j":["cop","man","officer","police","law","legal","enforcement","arrest","911"]},"woman-police-officer":{"a":"Woman Police Officer","b":"1F46E-200D-2640-FE0F","j":["cop","officer","police","woman","law","legal","enforcement","arrest","911","female"]},"detective":{"a":"Detective","b":"1F575-FE0F","j":["sleuth","spy","human"]},"man-detective":{"a":"Man Detective","b":"1F575-FE0F-200D-2642-FE0F","j":["detective","man","sleuth","spy","crime"]},"woman-detective":{"a":"Woman Detective","b":"1F575-FE0F-200D-2640-FE0F","j":["detective","sleuth","spy","woman","human","female"]},"guard":{"a":"Guard","b":"1F482","j":["protect"]},"man-guard":{"a":"Man Guard","b":"1F482-200D-2642-FE0F","j":["guard","man","uk","gb","british","male","guy","royal"]},"woman-guard":{"a":"Woman Guard","b":"1F482-200D-2640-FE0F","j":["guard","woman","uk","gb","british","female","royal"]},"ninja":{"a":"Ninja","b":"1F977","j":["fighter","hidden","stealth","ninjutsu","skills","japanese"]},"construction-worker":{"a":"Construction Worker","b":"1F477","j":["construction","hat","worker","labor","build"]},"man-construction-worker":{"a":"Man Construction Worker","b":"1F477-200D-2642-FE0F","j":["construction","man","worker","male","human","wip","guy","build","labor"]},"woman-construction-worker":{"a":"Woman Construction Worker","b":"1F477-200D-2640-FE0F","j":["construction","woman","worker","female","human","wip","build","labor"]},"person-with-crown":{"a":"Person with Crown","b":"1FAC5","j":["monarch","noble","regal","royalty","power"]},"prince":{"a":"Prince","b":"1F934","j":["boy","man","male","crown","royal","king"]},"princess":{"a":"Princess","b":"1F478","j":["fairy tale","fantasy","girl","woman","female","blond","crown","royal","queen"]},"person-wearing-turban":{"a":"Person Wearing Turban","b":"1F473","j":["turban","headdress"]},"man-wearing-turban":{"a":"Man Wearing Turban","b":"1F473-200D-2642-FE0F","j":["man","turban","male","indian","hinduism","arabs"]},"woman-wearing-turban":{"a":"Woman Wearing Turban","b":"1F473-200D-2640-FE0F","j":["turban","woman","female","indian","hinduism","arabs"]},"person-with-skullcap":{"a":"Person with Skullcap","b":"1F472","j":["cap","gua pi mao","hat","person","skullcap","man_with_skullcap","male","boy","chinese"]},"woman-with-headscarf":{"a":"Woman with Headscarf","b":"1F9D5","j":["headscarf","hijab","mantilla","tichel","bandana","head kerchief","female"]},"person-in-tuxedo":{"a":"Person in Tuxedo","b":"1F935","j":["groom","person","tuxedo","man_in_tuxedo","couple","marriage","wedding"]},"man-in-tuxedo":{"a":"Man in Tuxedo","b":"1F935-200D-2642-FE0F","j":["man","tuxedo","formal","fashion"]},"woman-in-tuxedo":{"a":"Woman in Tuxedo","b":"1F935-200D-2640-FE0F","j":["tuxedo","woman","formal","fashion"]},"person-with-veil":{"a":"Person with Veil","b":"1F470","j":["bride","person","veil","wedding","bride_with_veil","couple","marriage","woman"]},"man-with-veil":{"a":"Man with Veil","b":"1F470-200D-2642-FE0F","j":["man","veil","wedding","marriage"]},"woman-with-veil":{"a":"Woman with Veil","b":"1F470-200D-2640-FE0F","j":["veil","woman","wedding","marriage"]},"pregnant-woman":{"a":"Pregnant Woman","b":"1F930","j":["pregnant","woman","baby"]},"pregnant-man":{"a":"Pregnant Man","b":"1FAC3","j":["belly","bloated","full","pregnant","baby"]},"pregnant-person":{"a":"Pregnant Person","b":"1FAC4","j":["belly","bloated","full","pregnant","baby"]},"breastfeeding":{"a":"Breast-Feeding","b":"1F931","j":["baby","breast","breast-feeding","nursing","breast_feeding"]},"woman-feeding-baby":{"a":"Woman Feeding Baby","b":"1F469-200D-1F37C","j":["baby","feeding","nursing","woman","birth","food"]},"man-feeding-baby":{"a":"Man Feeding Baby","b":"1F468-200D-1F37C","j":["baby","feeding","man","nursing","birth","food"]},"person-feeding-baby":{"a":"Person Feeding Baby","b":"1F9D1-200D-1F37C","j":["baby","feeding","nursing","person","birth","food"]},"baby-angel":{"a":"Baby Angel","b":"1F47C","j":["angel","baby","face","fairy tale","fantasy","heaven","wings","halo"]},"santa-claus":{"a":"Santa Claus","b":"1F385","j":["celebration","Christmas","claus","father","santa","festival","man","male","xmas","father christmas"]},"mrs-claus":{"a":"Mrs. Claus","b":"1F936","j":["celebration","Christmas","claus","mother","Mrs.","woman","female","xmas","mother christmas"]},"mx-claus":{"a":"Mx Claus","b":"1F9D1-200D-1F384","j":["Claus, christmas","christmas"]},"superhero":{"a":"Superhero","b":"1F9B8","j":["good","hero","heroine","superpower","marvel"]},"man-superhero":{"a":"Man Superhero","b":"1F9B8-200D-2642-FE0F","j":["good","hero","man","superpower","male","superpowers"]},"woman-superhero":{"a":"Woman Superhero","b":"1F9B8-200D-2640-FE0F","j":["good","hero","heroine","superpower","woman","female","superpowers"]},"supervillain":{"a":"Supervillain","b":"1F9B9","j":["criminal","evil","superpower","villain","marvel"]},"man-supervillain":{"a":"Man Supervillain","b":"1F9B9-200D-2642-FE0F","j":["criminal","evil","man","superpower","villain","male","bad","hero","superpowers"]},"woman-supervillain":{"a":"Woman Supervillain","b":"1F9B9-200D-2640-FE0F","j":["criminal","evil","superpower","villain","woman","female","bad","heroine","superpowers"]},"mage":{"a":"Mage","b":"1F9D9","j":["sorcerer","sorceress","witch","wizard","magic"]},"man-mage":{"a":"Man Mage","b":"1F9D9-200D-2642-FE0F","j":["sorcerer","wizard","man","male","mage"]},"woman-mage":{"a":"Woman Mage","b":"1F9D9-200D-2640-FE0F","j":["sorceress","witch","woman","female","mage"]},"fairy":{"a":"Fairy","b":"1F9DA","j":["Oberon","Puck","Titania","wings","magical"]},"man-fairy":{"a":"Man Fairy","b":"1F9DA-200D-2642-FE0F","j":["Oberon","Puck","man","male"]},"woman-fairy":{"a":"Woman Fairy","b":"1F9DA-200D-2640-FE0F","j":["Titania","woman","female"]},"vampire":{"a":"Vampire","b":"1F9DB","j":["Dracula","undead","blood","twilight"]},"man-vampire":{"a":"Man Vampire","b":"1F9DB-200D-2642-FE0F","j":["Dracula","undead","man","male","dracula"]},"woman-vampire":{"a":"Woman Vampire","b":"1F9DB-200D-2640-FE0F","j":["undead","woman","female"]},"merperson":{"a":"Merperson","b":"1F9DC","j":["mermaid","merman","merwoman","sea"]},"merman":{"a":"Merman","b":"1F9DC-200D-2642-FE0F","j":["Triton","man","male","triton"]},"mermaid":{"a":"Mermaid","b":"1F9DC-200D-2640-FE0F","j":["merwoman","woman","female","ariel"]},"elf":{"a":"Elf","b":"1F9DD","j":["magical","LOTR style"]},"man-elf":{"a":"Man Elf","b":"1F9DD-200D-2642-FE0F","j":["magical","man","male"]},"woman-elf":{"a":"Woman Elf","b":"1F9DD-200D-2640-FE0F","j":["magical","woman","female"]},"genie":{"a":"Genie","b":"1F9DE","j":["djinn","(non-human color)","magical","wishes"]},"man-genie":{"a":"Man Genie","b":"1F9DE-200D-2642-FE0F","j":["djinn","man","male"]},"woman-genie":{"a":"Woman Genie","b":"1F9DE-200D-2640-FE0F","j":["djinn","woman","female"]},"zombie":{"a":"Zombie","b":"1F9DF","j":["undead","walking dead","(non-human color)","dead"]},"man-zombie":{"a":"Man Zombie","b":"1F9DF-200D-2642-FE0F","j":["undead","walking dead","man","male","dracula"]},"woman-zombie":{"a":"Woman Zombie","b":"1F9DF-200D-2640-FE0F","j":["undead","walking dead","woman","female"]},"troll":{"a":"Troll","b":"1F9CC","j":["fairy tale","fantasy","monster","mystical"]},"person-getting-massage":{"a":"Person Getting Massage","b":"1F486","j":["face","massage","salon","relax"]},"man-getting-massage":{"a":"Man Getting Massage","b":"1F486-200D-2642-FE0F","j":["face","man","massage","male","boy","head"]},"woman-getting-massage":{"a":"Woman Getting Massage","b":"1F486-200D-2640-FE0F","j":["face","massage","woman","female","girl","head"]},"person-getting-haircut":{"a":"Person Getting Haircut","b":"1F487","j":["barber","beauty","haircut","parlor","hairstyle"]},"man-getting-haircut":{"a":"Man Getting Haircut","b":"1F487-200D-2642-FE0F","j":["haircut","man","male","boy"]},"woman-getting-haircut":{"a":"Woman Getting Haircut","b":"1F487-200D-2640-FE0F","j":["haircut","woman","female","girl"]},"person-walking":{"a":"Person Walking","b":"1F6B6","j":["hike","walk","walking","move"]},"man-walking":{"a":"Man Walking","b":"1F6B6-200D-2642-FE0F","j":["hike","man","walk","human","feet","steps"]},"woman-walking":{"a":"Woman Walking","b":"1F6B6-200D-2640-FE0F","j":["hike","walk","woman","human","feet","steps","female"]},"person-standing":{"a":"Person Standing","b":"1F9CD","j":["stand","standing","still"]},"man-standing":{"a":"Man Standing","b":"1F9CD-200D-2642-FE0F","j":["man","standing","still"]},"woman-standing":{"a":"Woman Standing","b":"1F9CD-200D-2640-FE0F","j":["standing","woman","still"]},"person-kneeling":{"a":"Person Kneeling","b":"1F9CE","j":["kneel","kneeling","pray","respectful"]},"man-kneeling":{"a":"Man Kneeling","b":"1F9CE-200D-2642-FE0F","j":["kneeling","man","pray","respectful"]},"woman-kneeling":{"a":"Woman Kneeling","b":"1F9CE-200D-2640-FE0F","j":["kneeling","woman","respectful","pray"]},"person-with-white-cane":{"a":"Person with White Cane","b":"1F9D1-200D-1F9AF","j":["accessibility","blind","person_with_probing_cane"]},"man-with-white-cane":{"a":"Man with White Cane","b":"1F468-200D-1F9AF","j":["accessibility","blind","man","man_with_probing_cane"]},"woman-with-white-cane":{"a":"Woman with White Cane","b":"1F469-200D-1F9AF","j":["accessibility","blind","woman","woman_with_probing_cane"]},"person-in-motorized-wheelchair":{"a":"Person in Motorized Wheelchair","b":"1F9D1-200D-1F9BC","j":["accessibility","wheelchair","disability"]},"man-in-motorized-wheelchair":{"a":"Man in Motorized Wheelchair","b":"1F468-200D-1F9BC","j":["accessibility","man","wheelchair","disability"]},"woman-in-motorized-wheelchair":{"a":"Woman in Motorized Wheelchair","b":"1F469-200D-1F9BC","j":["accessibility","wheelchair","woman","disability"]},"person-in-manual-wheelchair":{"a":"Person in Manual Wheelchair","b":"1F9D1-200D-1F9BD","j":["accessibility","wheelchair","disability"]},"man-in-manual-wheelchair":{"a":"Man in Manual Wheelchair","b":"1F468-200D-1F9BD","j":["accessibility","man","wheelchair","disability"]},"woman-in-manual-wheelchair":{"a":"Woman in Manual Wheelchair","b":"1F469-200D-1F9BD","j":["accessibility","wheelchair","woman","disability"]},"person-running":{"a":"Person Running","b":"1F3C3","j":["marathon","running","move"]},"man-running":{"a":"Man Running","b":"1F3C3-200D-2642-FE0F","j":["man","marathon","racing","running","walking","exercise","race"]},"woman-running":{"a":"Woman Running","b":"1F3C3-200D-2640-FE0F","j":["marathon","racing","running","woman","walking","exercise","race","female"]},"woman-dancing":{"a":"Woman Dancing","b":"1F483","j":["dance","dancing","woman","female","girl","fun"]},"man-dancing":{"a":"Man Dancing","b":"1F57A","j":["dance","dancing","man","male","boy","fun","dancer"]},"person-in-suit-levitating":{"a":"Person in Suit Levitating","b":"1F574-FE0F","j":["business","person","suit","man_in_suit_levitating","levitate","hover","jump"]},"people-with-bunny-ears":{"a":"People with Bunny Ears","b":"1F46F","j":["bunny ear","dancer","partying","perform","costume"]},"men-with-bunny-ears":{"a":"Men with Bunny Ears","b":"1F46F-200D-2642-FE0F","j":["bunny ear","dancer","men","partying","male","bunny","boys"]},"women-with-bunny-ears":{"a":"Women with Bunny Ears","b":"1F46F-200D-2640-FE0F","j":["bunny ear","dancer","partying","women","female","bunny","girls"]},"person-in-steamy-room":{"a":"Person in Steamy Room","b":"1F9D6","j":["sauna","steam room","hamam","steambath","relax","spa"]},"man-in-steamy-room":{"a":"Man in Steamy Room","b":"1F9D6-200D-2642-FE0F","j":["sauna","steam room","male","man","spa","steamroom"]},"woman-in-steamy-room":{"a":"Woman in Steamy Room","b":"1F9D6-200D-2640-FE0F","j":["sauna","steam room","female","woman","spa","steamroom"]},"person-climbing":{"a":"Person Climbing","b":"1F9D7","j":["climber","sport"]},"man-climbing":{"a":"Man Climbing","b":"1F9D7-200D-2642-FE0F","j":["climber","sports","hobby","man","male","rock"]},"woman-climbing":{"a":"Woman Climbing","b":"1F9D7-200D-2640-FE0F","j":["climber","sports","hobby","woman","female","rock"]},"person-fencing":{"a":"Person Fencing","b":"1F93A","j":["fencer","fencing","sword","sports"]},"horse-racing":{"a":"Horse Racing","b":"1F3C7","j":["horse","jockey","racehorse","racing","animal","betting","competition","gambling","luck"]},"skier":{"a":"Skier","b":"26F7-FE0F","j":["ski","snow","sports","winter"]},"snowboarder":{"a":"Snowboarder","b":"1F3C2-FE0F","j":["ski","snow","snowboard","sports","winter"]},"person-golfing":{"a":"Person Golfing","b":"1F3CC-FE0F","j":["ball","golf","sports","business"]},"man-golfing":{"a":"Man Golfing","b":"1F3CC-FE0F-200D-2642-FE0F","j":["golf","man","sport"]},"woman-golfing":{"a":"Woman Golfing","b":"1F3CC-FE0F-200D-2640-FE0F","j":["golf","woman","sports","business","female"]},"person-surfing":{"a":"Person Surfing","b":"1F3C4-FE0F","j":["surfing","sport","sea"]},"man-surfing":{"a":"Man Surfing","b":"1F3C4-200D-2642-FE0F","j":["man","surfing","sports","ocean","sea","summer","beach"]},"woman-surfing":{"a":"Woman Surfing","b":"1F3C4-200D-2640-FE0F","j":["surfing","woman","sports","ocean","sea","summer","beach","female"]},"person-rowing-boat":{"a":"Person Rowing Boat","b":"1F6A3","j":["boat","rowboat","sport","move"]},"man-rowing-boat":{"a":"Man Rowing Boat","b":"1F6A3-200D-2642-FE0F","j":["boat","man","rowboat","sports","hobby","water","ship"]},"woman-rowing-boat":{"a":"Woman Rowing Boat","b":"1F6A3-200D-2640-FE0F","j":["boat","rowboat","woman","sports","hobby","water","ship","female"]},"person-swimming":{"a":"Person Swimming","b":"1F3CA-FE0F","j":["swim","sport","pool"]},"man-swimming":{"a":"Man Swimming","b":"1F3CA-200D-2642-FE0F","j":["man","swim","sports","exercise","human","athlete","water","summer"]},"woman-swimming":{"a":"Woman Swimming","b":"1F3CA-200D-2640-FE0F","j":["swim","woman","sports","exercise","human","athlete","water","summer","female"]},"person-bouncing-ball":{"a":"Person Bouncing Ball","b":"26F9-FE0F","j":["ball","sports","human"]},"man-bouncing-ball":{"a":"Man Bouncing Ball","b":"26F9-FE0F-200D-2642-FE0F","j":["ball","man","sport"]},"woman-bouncing-ball":{"a":"Woman Bouncing Ball","b":"26F9-FE0F-200D-2640-FE0F","j":["ball","woman","sports","human","female"]},"person-lifting-weights":{"a":"Person Lifting Weights","b":"1F3CB-FE0F","j":["lifter","weight","sports","training","exercise"]},"man-lifting-weights":{"a":"Man Lifting Weights","b":"1F3CB-FE0F-200D-2642-FE0F","j":["man","weight lifter","sport"]},"woman-lifting-weights":{"a":"Woman Lifting Weights","b":"1F3CB-FE0F-200D-2640-FE0F","j":["weight lifter","woman","sports","training","exercise","female"]},"person-biking":{"a":"Person Biking","b":"1F6B4","j":["bicycle","biking","cyclist","bike","sport","move"]},"man-biking":{"a":"Man Biking","b":"1F6B4-200D-2642-FE0F","j":["bicycle","biking","cyclist","man","bike","sports","exercise","hipster"]},"woman-biking":{"a":"Woman Biking","b":"1F6B4-200D-2640-FE0F","j":["bicycle","biking","cyclist","woman","bike","sports","exercise","hipster","female"]},"person-mountain-biking":{"a":"Person Mountain Biking","b":"1F6B5","j":["bicycle","bicyclist","bike","cyclist","mountain","sport","move"]},"man-mountain-biking":{"a":"Man Mountain Biking","b":"1F6B5-200D-2642-FE0F","j":["bicycle","bike","cyclist","man","mountain","transportation","sports","human","race"]},"woman-mountain-biking":{"a":"Woman Mountain Biking","b":"1F6B5-200D-2640-FE0F","j":["bicycle","bike","biking","cyclist","mountain","woman","transportation","sports","human","race","female"]},"person-cartwheeling":{"a":"Person Cartwheeling","b":"1F938","j":["cartwheel","gymnastics","sport","gymnastic"]},"man-cartwheeling":{"a":"Man Cartwheeling","b":"1F938-200D-2642-FE0F","j":["cartwheel","gymnastics","man"]},"woman-cartwheeling":{"a":"Woman Cartwheeling","b":"1F938-200D-2640-FE0F","j":["cartwheel","gymnastics","woman"]},"people-wrestling":{"a":"People Wrestling","b":"1F93C","j":["wrestle","wrestler","sport"]},"men-wrestling":{"a":"Men Wrestling","b":"1F93C-200D-2642-FE0F","j":["men","wrestle","sports","wrestlers"]},"women-wrestling":{"a":"Women Wrestling","b":"1F93C-200D-2640-FE0F","j":["women","wrestle","sports","wrestlers"]},"person-playing-water-polo":{"a":"Person Playing Water Polo","b":"1F93D","j":["polo","water","sport"]},"man-playing-water-polo":{"a":"Man Playing Water Polo","b":"1F93D-200D-2642-FE0F","j":["man","water polo","sports","pool"]},"woman-playing-water-polo":{"a":"Woman Playing Water Polo","b":"1F93D-200D-2640-FE0F","j":["water polo","woman","sports","pool"]},"person-playing-handball":{"a":"Person Playing Handball","b":"1F93E","j":["ball","handball","sport"]},"man-playing-handball":{"a":"Man Playing Handball","b":"1F93E-200D-2642-FE0F","j":["handball","man","sports"]},"woman-playing-handball":{"a":"Woman Playing Handball","b":"1F93E-200D-2640-FE0F","j":["handball","woman","sports"]},"person-juggling":{"a":"Person Juggling","b":"1F939","j":["balance","juggle","multitask","skill","performance"]},"man-juggling":{"a":"Man Juggling","b":"1F939-200D-2642-FE0F","j":["juggling","man","multitask","juggle","balance","skill"]},"woman-juggling":{"a":"Woman Juggling","b":"1F939-200D-2640-FE0F","j":["juggling","multitask","woman","juggle","balance","skill"]},"person-in-lotus-position":{"a":"Person in Lotus Position","b":"1F9D8","j":["meditation","yoga","serenity","meditate"]},"man-in-lotus-position":{"a":"Man in Lotus Position","b":"1F9D8-200D-2642-FE0F","j":["meditation","yoga","man","male","serenity","zen","mindfulness"]},"woman-in-lotus-position":{"a":"Woman in Lotus Position","b":"1F9D8-200D-2640-FE0F","j":["meditation","yoga","woman","female","serenity","zen","mindfulness"]},"person-taking-bath":{"a":"Person Taking Bath","b":"1F6C0","j":["bath","bathtub","clean","shower","bathroom"]},"person-in-bed":{"a":"Person in Bed","b":"1F6CC","j":["good night","hotel","sleep","bed","rest"]},"people-holding-hands":{"a":"People Holding Hands","b":"1F9D1-200D-1F91D-200D-1F9D1","j":["couple","hand","hold","holding hands","person","friendship"]},"women-holding-hands":{"a":"Women Holding Hands","b":"1F46D","j":["couple","hand","holding hands","women","pair","friendship","love","like","female","people","human"]},"woman-and-man-holding-hands":{"a":"Woman and Man Holding Hands","b":"1F46B","j":["couple","hand","hold","holding hands","man","woman","pair","people","human","love","date","dating","like","affection","valentines","marriage"]},"men-holding-hands":{"a":"Men Holding Hands","b":"1F46C","j":["couple","Gemini","holding hands","man","men","twins","zodiac","pair","love","like","bromance","friendship","people","human"]},"kiss":{"a":"Kiss","b":"1F48F","j":["couple","pair","valentines","love","like","dating","marriage"]},"kiss-woman-man":{"a":"Kiss: Woman, Man","b":"1F469-200D-2764-FE0F-200D-1F48B-200D-1F468","j":["couple","kiss","man","woman","love"]},"kiss-man-man":{"a":"Kiss: Man, Man","b":"1F468-200D-2764-FE0F-200D-1F48B-200D-1F468","j":["couple","kiss","man","pair","valentines","love","like","dating","marriage"]},"kiss-woman-woman":{"a":"Kiss: Woman, Woman","b":"1F469-200D-2764-FE0F-200D-1F48B-200D-1F469","j":["couple","kiss","woman","pair","valentines","love","like","dating","marriage"]},"couple-with-heart":{"a":"Couple with Heart","b":"1F491","j":["couple","love","pair","like","affection","human","dating","valentines","marriage"]},"couple-with-heart-woman-man":{"a":"Couple with Heart: Woman, Man","b":"1F469-200D-2764-FE0F-200D-1F468","j":["couple","couple with heart","love","man","woman"]},"couple-with-heart-man-man":{"a":"Couple with Heart: Man, Man","b":"1F468-200D-2764-FE0F-200D-1F468","j":["couple","couple with heart","love","man","pair","like","affection","human","dating","valentines","marriage"]},"couple-with-heart-woman-woman":{"a":"Couple with Heart: Woman, Woman","b":"1F469-200D-2764-FE0F-200D-1F469","j":["couple","couple with heart","love","woman","pair","like","affection","human","dating","valentines","marriage"]},"family":{"a":"Family","b":"1F46A-FE0F","j":["home","parents","child","mom","dad","father","mother","people","human"]},"family-man-woman-boy":{"a":"Family: Man, Woman, Boy","b":"1F468-200D-1F469-200D-1F466","j":["boy","family","man","woman","love"]},"family-man-woman-girl":{"a":"Family: Man, Woman, Girl","b":"1F468-200D-1F469-200D-1F467","j":["family","girl","man","woman","home","parents","people","human","child"]},"family-man-woman-girl-boy":{"a":"Family: Man, Woman, Girl, Boy","b":"1F468-200D-1F469-200D-1F467-200D-1F466","j":["boy","family","girl","man","woman","home","parents","people","human","children"]},"family-man-woman-boy-boy":{"a":"Family: Man, Woman, Boy, Boy","b":"1F468-200D-1F469-200D-1F466-200D-1F466","j":["boy","family","man","woman","home","parents","people","human","children"]},"family-man-woman-girl-girl":{"a":"Family: Man, Woman, Girl, Girl","b":"1F468-200D-1F469-200D-1F467-200D-1F467","j":["family","girl","man","woman","home","parents","people","human","children"]},"family-man-man-boy":{"a":"Family: Man, Man, Boy","b":"1F468-200D-1F468-200D-1F466","j":["boy","family","man","home","parents","people","human","children"]},"family-man-man-girl":{"a":"Family: Man, Man, Girl","b":"1F468-200D-1F468-200D-1F467","j":["family","girl","man","home","parents","people","human","children"]},"family-man-man-girl-boy":{"a":"Family: Man, Man, Girl, Boy","b":"1F468-200D-1F468-200D-1F467-200D-1F466","j":["boy","family","girl","man","home","parents","people","human","children"]},"family-man-man-boy-boy":{"a":"Family: Man, Man, Boy, Boy","b":"1F468-200D-1F468-200D-1F466-200D-1F466","j":["boy","family","man","home","parents","people","human","children"]},"family-man-man-girl-girl":{"a":"Family: Man, Man, Girl, Girl","b":"1F468-200D-1F468-200D-1F467-200D-1F467","j":["family","girl","man","home","parents","people","human","children"]},"family-woman-woman-boy":{"a":"Family: Woman, Woman, Boy","b":"1F469-200D-1F469-200D-1F466","j":["boy","family","woman","home","parents","people","human","children"]},"family-woman-woman-girl":{"a":"Family: Woman, Woman, Girl","b":"1F469-200D-1F469-200D-1F467","j":["family","girl","woman","home","parents","people","human","children"]},"family-woman-woman-girl-boy":{"a":"Family: Woman, Woman, Girl, Boy","b":"1F469-200D-1F469-200D-1F467-200D-1F466","j":["boy","family","girl","woman","home","parents","people","human","children"]},"family-woman-woman-boy-boy":{"a":"Family: Woman, Woman, Boy, Boy","b":"1F469-200D-1F469-200D-1F466-200D-1F466","j":["boy","family","woman","home","parents","people","human","children"]},"family-woman-woman-girl-girl":{"a":"Family: Woman, Woman, Girl, Girl","b":"1F469-200D-1F469-200D-1F467-200D-1F467","j":["family","girl","woman","home","parents","people","human","children"]},"family-man-boy":{"a":"Family: Man, Boy","b":"1F468-200D-1F466","j":["boy","family","man","home","parent","people","human","child"]},"family-man-boy-boy":{"a":"Family: Man, Boy, Boy","b":"1F468-200D-1F466-200D-1F466","j":["boy","family","man","home","parent","people","human","children"]},"family-man-girl":{"a":"Family: Man, Girl","b":"1F468-200D-1F467","j":["family","girl","man","home","parent","people","human","child"]},"family-man-girl-boy":{"a":"Family: Man, Girl, Boy","b":"1F468-200D-1F467-200D-1F466","j":["boy","family","girl","man","home","parent","people","human","children"]},"family-man-girl-girl":{"a":"Family: Man, Girl, Girl","b":"1F468-200D-1F467-200D-1F467","j":["family","girl","man","home","parent","people","human","children"]},"family-woman-boy":{"a":"Family: Woman, Boy","b":"1F469-200D-1F466","j":["boy","family","woman","home","parent","people","human","child"]},"family-woman-boy-boy":{"a":"Family: Woman, Boy, Boy","b":"1F469-200D-1F466-200D-1F466","j":["boy","family","woman","home","parent","people","human","children"]},"family-woman-girl":{"a":"Family: Woman, Girl","b":"1F469-200D-1F467","j":["family","girl","woman","home","parent","people","human","child"]},"family-woman-girl-boy":{"a":"Family: Woman, Girl, Boy","b":"1F469-200D-1F467-200D-1F466","j":["boy","family","girl","woman","home","parent","people","human","children"]},"family-woman-girl-girl":{"a":"Family: Woman, Girl, Girl","b":"1F469-200D-1F467-200D-1F467","j":["family","girl","woman","home","parent","people","human","children"]},"speaking-head":{"a":"Speaking Head","b":"1F5E3-FE0F","j":["face","head","silhouette","speak","speaking","user","person","human","sing","say","talk"]},"bust-in-silhouette":{"a":"Bust in Silhouette","b":"1F464","j":["bust","silhouette","user","person","human"]},"busts-in-silhouette":{"a":"Busts in Silhouette","b":"1F465","j":["bust","silhouette","user","person","human","group","team"]},"people-hugging":{"a":"People Hugging","b":"1FAC2","j":["goodbye","hello","hug","thanks","care"]},"footprints":{"a":"Footprints","b":"1F463","j":["clothing","footprint","print","feet","tracking","walking","beach"]},"red-hair":{"a":"Red Hair","b":"1F9B0","j":["ginger","red hair","redhead"]},"curly-hair":{"a":"Curly Hair","b":"1F9B1","j":["afro","curly","curly hair","ringlets"]},"white-hair":{"a":"White Hair","b":"1F9B3","j":["gray","hair","old","white"]},"bald":{"a":"Bald","b":"1F9B2","j":["bald","chemotherapy","hairless","no hair","shaven"]},"monkey-face":{"a":"Monkey Face","b":"1F435","j":["face","monkey","animal","nature","circus"]},"monkey":{"a":"Monkey","b":"1F412","j":["animal","nature","banana","circus"]},"gorilla":{"a":"Gorilla","b":"1F98D","j":["animal","nature","circus"]},"orangutan":{"a":"Orangutan","b":"1F9A7","j":["ape","animal"]},"dog-face":{"a":"Dog Face","b":"1F436","j":["dog","face","pet","animal","friend","nature","woof","puppy","faithful"]},"dog":{"a":"Dog","b":"1F415-FE0F","j":["pet","animal","nature","friend","doge","faithful"]},"guide-dog":{"a":"Guide Dog","b":"1F9AE","j":["accessibility","blind","guide","animal"]},"service-dog":{"a":"Service Dog","b":"1F415-200D-1F9BA","j":["accessibility","assistance","dog","service","blind","animal"]},"poodle":{"a":"Poodle","b":"1F429","j":["dog","animal","101","nature","pet"]},"wolf":{"a":"Wolf","b":"1F43A","j":["face","animal","nature","wild"]},"fox":{"a":"Fox","b":"1F98A","j":["face","animal","nature"]},"raccoon":{"a":"Raccoon","b":"1F99D","j":["curious","sly","animal","nature"]},"cat-face":{"a":"Cat Face","b":"1F431","j":["cat","face","pet","animal","meow","nature","kitten"]},"cat":{"a":"Cat","b":"1F408-FE0F","j":["pet","animal","meow","cats"]},"black-cat":{"a":"Black Cat","b":"1F408-200D-2B1B","j":["black","cat","unlucky","superstition","luck"]},"lion":{"a":"Lion","b":"1F981","j":["face","Leo","zodiac","animal","nature"]},"tiger-face":{"a":"Tiger Face","b":"1F42F","j":["face","tiger","animal","cat","danger","wild","nature","roar"]},"tiger":{"a":"Tiger","b":"1F405","j":["animal","nature","roar"]},"leopard":{"a":"Leopard","b":"1F406","j":["animal","nature"]},"horse-face":{"a":"Horse Face","b":"1F434","j":["face","horse","animal","brown","nature"]},"moose":{"a":"⊛ Moose","b":"1FACE","j":["animal","antlers","elk","mammal","shrek","canada","sweden","sven","cool"]},"donkey":{"a":"⊛ Donkey","b":"1FACF","j":["animal","ass","burro","mammal","mule","stubborn","eeyore"]},"horse":{"a":"Horse","b":"1F40E","j":["equestrian","racehorse","racing","animal","gamble","luck"]},"unicorn":{"a":"Unicorn","b":"1F984","j":["face","animal","nature","mystical"]},"zebra":{"a":"Zebra","b":"1F993","j":["stripe","animal","nature","stripes","safari"]},"deer":{"a":"Deer","b":"1F98C","j":["animal","nature","horns","venison"]},"bison":{"a":"Bison","b":"1F9AC","j":["buffalo","herd","wisent","ox"]},"cow-face":{"a":"Cow Face","b":"1F42E","j":["cow","face","beef","ox","animal","nature","moo","milk"]},"ox":{"a":"Ox","b":"1F402","j":["bull","Taurus","zodiac","animal","cow","beef"]},"water-buffalo":{"a":"Water Buffalo","b":"1F403","j":["buffalo","water","animal","nature","ox","cow"]},"cow":{"a":"Cow","b":"1F404","j":["beef","ox","animal","nature","moo","milk"]},"pig-face":{"a":"Pig Face","b":"1F437","j":["face","pig","animal","oink","nature"]},"pig":{"a":"Pig","b":"1F416","j":["sow","animal","nature"]},"boar":{"a":"Boar","b":"1F417","j":["pig","animal","nature"]},"pig-nose":{"a":"Pig Nose","b":"1F43D","j":["face","nose","pig","animal","oink"]},"ram":{"a":"Ram","b":"1F40F","j":["Aries","male","sheep","zodiac","animal","nature"]},"ewe":{"a":"Ewe","b":"1F411","j":["female","sheep","animal","nature","wool","shipit"]},"goat":{"a":"Goat","b":"1F410","j":["Capricorn","zodiac","animal","nature"]},"camel":{"a":"Camel","b":"1F42A","j":["dromedary","hump","animal","hot","desert"]},"twohump-camel":{"a":"Two-Hump Camel","b":"1F42B","j":["bactrian","camel","hump","two-hump camel","two_hump_camel","animal","nature","hot","desert"]},"llama":{"a":"Llama","b":"1F999","j":["alpaca","guanaco","vicuña","wool","animal","nature"]},"giraffe":{"a":"Giraffe","b":"1F992","j":["spots","animal","nature","safari"]},"elephant":{"a":"Elephant","b":"1F418","j":["animal","nature","nose","th","circus"]},"mammoth":{"a":"Mammoth","b":"1F9A3","j":["extinction","large","tusk","woolly","elephant","tusks"]},"rhinoceros":{"a":"Rhinoceros","b":"1F98F","j":["animal","nature","horn"]},"hippopotamus":{"a":"Hippopotamus","b":"1F99B","j":["hippo","animal","nature"]},"mouse-face":{"a":"Mouse Face","b":"1F42D","j":["face","mouse","animal","nature","cheese_wedge","rodent"]},"mouse":{"a":"Mouse","b":"1F401","j":["animal","nature","rodent"]},"rat":{"a":"Rat","b":"1F400","j":["animal","mouse","rodent"]},"hamster":{"a":"Hamster","b":"1F439","j":["face","pet","animal","nature"]},"rabbit-face":{"a":"Rabbit Face","b":"1F430","j":["bunny","face","pet","rabbit","animal","nature","spring","magic"]},"rabbit":{"a":"Rabbit","b":"1F407","j":["bunny","pet","animal","nature","magic","spring"]},"chipmunk":{"a":"Chipmunk","b":"1F43F-FE0F","j":["squirrel","animal","nature","rodent"]},"beaver":{"a":"Beaver","b":"1F9AB","j":["dam","animal","rodent"]},"hedgehog":{"a":"Hedgehog","b":"1F994","j":["spiny","animal","nature"]},"bat":{"a":"Bat","b":"1F987","j":["vampire","animal","nature","blind"]},"bear":{"a":"Bear","b":"1F43B","j":["face","animal","nature","wild"]},"polar-bear":{"a":"Polar Bear","b":"1F43B-200D-2744-FE0F","j":["arctic","bear","white","animal"]},"koala":{"a":"Koala","b":"1F428","j":["face","marsupial","animal","nature"]},"panda":{"a":"Panda","b":"1F43C","j":["face","animal","nature"]},"sloth":{"a":"Sloth","b":"1F9A5","j":["lazy","slow","animal"]},"otter":{"a":"Otter","b":"1F9A6","j":["fishing","playful","animal"]},"skunk":{"a":"Skunk","b":"1F9A8","j":["stink","animal"]},"kangaroo":{"a":"Kangaroo","b":"1F998","j":["Australia","joey","jump","marsupial","animal","nature","australia","hop"]},"badger":{"a":"Badger","b":"1F9A1","j":["honey badger","pester","animal","nature","honey"]},"paw-prints":{"a":"Paw Prints","b":"1F43E","j":["feet","paw","print","animal","tracking","footprints","dog","cat","pet"]},"turkey":{"a":"Turkey","b":"1F983","j":["bird","animal"]},"chicken":{"a":"Chicken","b":"1F414","j":["bird","animal","cluck","nature"]},"rooster":{"a":"Rooster","b":"1F413","j":["bird","animal","nature","chicken"]},"hatching-chick":{"a":"Hatching Chick","b":"1F423","j":["baby","bird","chick","hatching","animal","chicken","egg","born"]},"baby-chick":{"a":"Baby Chick","b":"1F424","j":["baby","bird","chick","animal","chicken"]},"frontfacing-baby-chick":{"a":"Front-Facing Baby Chick","b":"1F425","j":["baby","bird","chick","front-facing baby chick","front_facing_baby_chick","animal","chicken"]},"bird":{"a":"Bird","b":"1F426-FE0F","j":["animal","nature","fly","tweet","spring"]},"penguin":{"a":"Penguin","b":"1F427","j":["bird","animal","nature"]},"dove":{"a":"Dove","b":"1F54A-FE0F","j":["bird","fly","peace","animal"]},"eagle":{"a":"Eagle","b":"1F985","j":["bird","animal","nature"]},"duck":{"a":"Duck","b":"1F986","j":["bird","animal","nature","mallard"]},"swan":{"a":"Swan","b":"1F9A2","j":["bird","cygnet","ugly duckling","animal","nature"]},"owl":{"a":"Owl","b":"1F989","j":["bird","wise","animal","nature","hoot"]},"dodo":{"a":"Dodo","b":"1F9A4","j":["extinction","large","Mauritius","animal","bird"]},"feather":{"a":"Feather","b":"1FAB6","j":["bird","flight","light","plumage","fly"]},"flamingo":{"a":"Flamingo","b":"1F9A9","j":["flamboyant","tropical","animal"]},"peacock":{"a":"Peacock","b":"1F99A","j":["bird","ostentatious","peahen","proud","animal","nature"]},"parrot":{"a":"Parrot","b":"1F99C","j":["bird","pirate","talk","animal","nature"]},"wing":{"a":"⊛ Wing","b":"1FABD","j":["angelic","aviation","bird","flying","mythology","angel","birds"]},"black-bird":{"a":"⊛ Black Bird","b":"1F426-200D-2B1B","j":["bird","black","crow","raven","rook"]},"goose":{"a":"⊛ Goose","b":"1FABF","j":["bird","fowl","honk","silly","jemima","goosebumps"]},"frog":{"a":"Frog","b":"1F438","j":["face","animal","nature","croak","toad"]},"crocodile":{"a":"Crocodile","b":"1F40A","j":["animal","nature","reptile","lizard","alligator"]},"turtle":{"a":"Turtle","b":"1F422","j":["terrapin","tortoise","animal","slow","nature"]},"lizard":{"a":"Lizard","b":"1F98E","j":["reptile","animal","nature"]},"snake":{"a":"Snake","b":"1F40D","j":["bearer","Ophiuchus","serpent","zodiac","animal","evil","nature","hiss","python"]},"dragon-face":{"a":"Dragon Face","b":"1F432","j":["dragon","face","fairy tale","animal","myth","nature","chinese","green"]},"dragon":{"a":"Dragon","b":"1F409","j":["fairy tale","animal","myth","nature","chinese","green"]},"sauropod":{"a":"Sauropod","b":"1F995","j":["brachiosaurus","brontosaurus","diplodocus","animal","nature","dinosaur","extinct"]},"trex":{"a":"T-Rex","b":"1F996","j":["Tyrannosaurus Rex","t_rex","animal","nature","dinosaur","tyrannosaurus","extinct"]},"spouting-whale":{"a":"Spouting Whale","b":"1F433","j":["face","spouting","whale","animal","nature","sea","ocean"]},"whale":{"a":"Whale","b":"1F40B","j":["animal","nature","sea","ocean"]},"dolphin":{"a":"Dolphin","b":"1F42C","j":["flipper","animal","nature","fish","sea","ocean","fins","beach"]},"seal":{"a":"Seal","b":"1F9AD","j":["sea lion","animal","creature","sea"]},"fish":{"a":"Fish","b":"1F41F-FE0F","j":["Pisces","zodiac","animal","food","nature"]},"tropical-fish":{"a":"Tropical Fish","b":"1F420","j":["fish","tropical","animal","swim","ocean","beach","nemo"]},"blowfish":{"a":"Blowfish","b":"1F421","j":["fish","animal","nature","food","sea","ocean"]},"shark":{"a":"Shark","b":"1F988","j":["fish","animal","nature","sea","ocean","jaws","fins","beach"]},"octopus":{"a":"Octopus","b":"1F419","j":["animal","creature","ocean","sea","nature","beach"]},"spiral-shell":{"a":"Spiral Shell","b":"1F41A","j":["shell","spiral","nature","sea","beach"]},"coral":{"a":"Coral","b":"1FAB8","j":["ocean","reef","sea"]},"jellyfish":{"a":"⊛ Jellyfish","b":"1FABC","j":["burn","invertebrate","jelly","marine","ouch","stinger","sting","tentacles"]},"snail":{"a":"Snail","b":"1F40C","j":["slow","animal","shell"]},"butterfly":{"a":"Butterfly","b":"1F98B","j":["insect","pretty","animal","nature","caterpillar"]},"bug":{"a":"Bug","b":"1F41B","j":["insect","animal","nature","worm"]},"ant":{"a":"Ant","b":"1F41C","j":["insect","animal","nature","bug"]},"honeybee":{"a":"Honeybee","b":"1F41D","j":["bee","insect","animal","nature","bug","spring","honey"]},"beetle":{"a":"Beetle","b":"1FAB2","j":["bug","insect"]},"lady-beetle":{"a":"Lady Beetle","b":"1F41E","j":["beetle","insect","ladybird","ladybug","animal","nature"]},"cricket":{"a":"Cricket","b":"1F997","j":["grasshopper","Orthoptera","animal","chirp"]},"cockroach":{"a":"Cockroach","b":"1FAB3","j":["insect","pest","roach","pests"]},"spider":{"a":"Spider","b":"1F577-FE0F","j":["insect","animal","arachnid"]},"spider-web":{"a":"Spider Web","b":"1F578-FE0F","j":["spider","web","animal","insect","arachnid","silk"]},"scorpion":{"a":"Scorpion","b":"1F982","j":["scorpio","Scorpio","zodiac","animal","arachnid"]},"mosquito":{"a":"Mosquito","b":"1F99F","j":["disease","fever","malaria","pest","virus","animal","nature","insect"]},"fly":{"a":"Fly","b":"1FAB0","j":["disease","maggot","pest","rotting","insect"]},"worm":{"a":"Worm","b":"1FAB1","j":["annelid","earthworm","parasite","animal"]},"microbe":{"a":"Microbe","b":"1F9A0","j":["amoeba","bacteria","virus","germs","covid"]},"bouquet":{"a":"Bouquet","b":"1F490","j":["flower","flowers","nature","spring"]},"cherry-blossom":{"a":"Cherry Blossom","b":"1F338","j":["blossom","cherry","flower","nature","plant","spring"]},"white-flower":{"a":"White Flower","b":"1F4AE","j":["flower","japanese","spring"]},"lotus":{"a":"Lotus","b":"1FAB7","j":["Buddhism","flower","Hinduism","India","purity","Vietnam","calm","meditation"]},"rosette":{"a":"Rosette","b":"1F3F5-FE0F","j":["plant","flower","decoration","military"]},"rose":{"a":"Rose","b":"1F339","j":["flower","flowers","valentines","love","spring"]},"wilted-flower":{"a":"Wilted Flower","b":"1F940","j":["flower","wilted","plant","nature","rose"]},"hibiscus":{"a":"Hibiscus","b":"1F33A","j":["flower","plant","vegetable","flowers","beach"]},"sunflower":{"a":"Sunflower","b":"1F33B","j":["flower","sun","nature","plant","fall"]},"blossom":{"a":"Blossom","b":"1F33C","j":["flower","nature","flowers","yellow"]},"tulip":{"a":"Tulip","b":"1F337","j":["flower","flowers","plant","nature","summer","spring"]},"hyacinth":{"a":"⊛ Hyacinth","b":"1FABB","j":["bluebonnet","flower","lavender","lupine","snapdragon"]},"seedling":{"a":"Seedling","b":"1F331","j":["young","plant","nature","grass","lawn","spring"]},"potted-plant":{"a":"Potted Plant","b":"1FAB4","j":["boring","grow","house","nurturing","plant","useless","greenery"]},"evergreen-tree":{"a":"Evergreen Tree","b":"1F332","j":["tree","plant","nature"]},"deciduous-tree":{"a":"Deciduous Tree","b":"1F333","j":["deciduous","shedding","tree","plant","nature"]},"palm-tree":{"a":"Palm Tree","b":"1F334","j":["palm","tree","plant","vegetable","nature","summer","beach","mojito","tropical"]},"cactus":{"a":"Cactus","b":"1F335","j":["plant","vegetable","nature"]},"sheaf-of-rice":{"a":"Sheaf of Rice","b":"1F33E","j":["ear","grain","rice","nature","plant"]},"herb":{"a":"Herb","b":"1F33F","j":["leaf","vegetable","plant","medicine","weed","grass","lawn"]},"shamrock":{"a":"Shamrock","b":"2618-FE0F","j":["plant","vegetable","nature","irish","clover"]},"four-leaf-clover":{"a":"Four Leaf Clover","b":"1F340","j":["4","clover","four","four-leaf clover","leaf","vegetable","plant","nature","lucky","irish"]},"maple-leaf":{"a":"Maple Leaf","b":"1F341","j":["falling","leaf","maple","nature","plant","vegetable","ca","fall"]},"fallen-leaf":{"a":"Fallen Leaf","b":"1F342","j":["falling","leaf","nature","plant","vegetable","leaves"]},"leaf-fluttering-in-wind":{"a":"Leaf Fluttering in Wind","b":"1F343","j":["blow","flutter","leaf","wind","nature","plant","tree","vegetable","grass","lawn","spring"]},"empty-nest":{"a":"Empty Nest","b":"1FAB9","j":["nesting","bird"]},"nest-with-eggs":{"a":"Nest with Eggs","b":"1FABA","j":["nesting","bird"]},"mushroom":{"a":"Mushroom","b":"1F344","j":["toadstool","plant","vegetable"]},"grapes":{"a":"Grapes","b":"1F347","j":["fruit","grape","food","wine"]},"melon":{"a":"Melon","b":"1F348","j":["fruit","nature","food"]},"watermelon":{"a":"Watermelon","b":"1F349","j":["fruit","food","picnic","summer"]},"tangerine":{"a":"Tangerine","b":"1F34A","j":["fruit","orange","food","nature"]},"lemon":{"a":"Lemon","b":"1F34B","j":["citrus","fruit","nature"]},"banana":{"a":"Banana","b":"1F34C","j":["fruit","food","monkey"]},"pineapple":{"a":"Pineapple","b":"1F34D","j":["fruit","nature","food"]},"mango":{"a":"Mango","b":"1F96D","j":["fruit","tropical","food"]},"red-apple":{"a":"Red Apple","b":"1F34E","j":["apple","fruit","red","mac","school"]},"green-apple":{"a":"Green Apple","b":"1F34F","j":["apple","fruit","green","nature"]},"pear":{"a":"Pear","b":"1F350","j":["fruit","nature","food"]},"peach":{"a":"Peach","b":"1F351","j":["fruit","nature","food"]},"cherries":{"a":"Cherries","b":"1F352","j":["berries","cherry","fruit","red","food"]},"strawberry":{"a":"Strawberry","b":"1F353","j":["berry","fruit","food","nature"]},"blueberries":{"a":"Blueberries","b":"1FAD0","j":["berry","bilberry","blue","blueberry","fruit"]},"kiwi-fruit":{"a":"Kiwi Fruit","b":"1F95D","j":["food","fruit","kiwi"]},"tomato":{"a":"Tomato","b":"1F345","j":["fruit","vegetable","nature","food"]},"olive":{"a":"Olive","b":"1FAD2","j":["food","fruit"]},"coconut":{"a":"Coconut","b":"1F965","j":["palm","piña colada","fruit","nature","food"]},"avocado":{"a":"Avocado","b":"1F951","j":["food","fruit"]},"eggplant":{"a":"Eggplant","b":"1F346","j":["aubergine","vegetable","nature","food"]},"potato":{"a":"Potato","b":"1F954","j":["food","vegetable","tuber","vegatable","starch"]},"carrot":{"a":"Carrot","b":"1F955","j":["food","vegetable","orange"]},"ear-of-corn":{"a":"Ear of Corn","b":"1F33D","j":["corn","ear","maize","maze","food","vegetable","plant"]},"hot-pepper":{"a":"Hot Pepper","b":"1F336-FE0F","j":["hot","pepper","food","spicy","chilli","chili"]},"bell-pepper":{"a":"Bell Pepper","b":"1FAD1","j":["capsicum","pepper","vegetable","fruit","plant"]},"cucumber":{"a":"Cucumber","b":"1F952","j":["food","pickle","vegetable","fruit"]},"leafy-green":{"a":"Leafy Green","b":"1F96C","j":["bok choy","cabbage","kale","lettuce","food","vegetable","plant"]},"broccoli":{"a":"Broccoli","b":"1F966","j":["wild cabbage","fruit","food","vegetable"]},"garlic":{"a":"Garlic","b":"1F9C4","j":["flavoring","food","spice","cook"]},"onion":{"a":"Onion","b":"1F9C5","j":["flavoring","cook","food","spice"]},"peanuts":{"a":"Peanuts","b":"1F95C","j":["food","nut","peanut","vegetable"]},"beans":{"a":"Beans","b":"1FAD8","j":["food","kidney","legume"]},"chestnut":{"a":"Chestnut","b":"1F330","j":["plant","food","squirrel"]},"ginger-root":{"a":"⊛ Ginger Root","b":"1FADA","j":["beer","root","spice","yellow","cooking","gingerbread"]},"pea-pod":{"a":"⊛ Pea Pod","b":"1FADB","j":["beans","edamame","legume","pea","pod","vegetable","cozy","green"]},"bread":{"a":"Bread","b":"1F35E","j":["loaf","food","wheat","breakfast","toast"]},"croissant":{"a":"Croissant","b":"1F950","j":["bread","breakfast","food","french","roll"]},"baguette-bread":{"a":"Baguette Bread","b":"1F956","j":["baguette","bread","food","french","france","bakery"]},"flatbread":{"a":"Flatbread","b":"1FAD3","j":["arepa","lavash","naan","pita","flour","food","bakery"]},"pretzel":{"a":"Pretzel","b":"1F968","j":["twisted","convoluted","food","bread","germany","bakery"]},"bagel":{"a":"Bagel","b":"1F96F","j":["bakery","breakfast","schmear","food","bread","jewish_bakery"]},"pancakes":{"a":"Pancakes","b":"1F95E","j":["breakfast","crêpe","food","hotcake","pancake","flapjacks","hotcakes","brunch"]},"waffle":{"a":"Waffle","b":"1F9C7","j":["breakfast","indecisive","iron","food","brunch"]},"cheese-wedge":{"a":"Cheese Wedge","b":"1F9C0","j":["cheese","food","chadder","swiss"]},"meat-on-bone":{"a":"Meat on Bone","b":"1F356","j":["bone","meat","good","food","drumstick"]},"poultry-leg":{"a":"Poultry Leg","b":"1F357","j":["bone","chicken","drumstick","leg","poultry","food","meat","bird","turkey"]},"cut-of-meat":{"a":"Cut of Meat","b":"1F969","j":["chop","lambchop","porkchop","steak","food","cow","meat","cut"]},"bacon":{"a":"Bacon","b":"1F953","j":["breakfast","food","meat","pork","pig","brunch"]},"hamburger":{"a":"Hamburger","b":"1F354","j":["burger","meat","fast food","beef","cheeseburger","mcdonalds","burger king"]},"french-fries":{"a":"French Fries","b":"1F35F","j":["french","fries","chips","snack","fast food","potato"]},"pizza":{"a":"Pizza","b":"1F355","j":["cheese","slice","food","party","italy"]},"hot-dog":{"a":"Hot Dog","b":"1F32D","j":["frankfurter","hotdog","sausage","food","america"]},"sandwich":{"a":"Sandwich","b":"1F96A","j":["bread","food","lunch","toast","bakery"]},"taco":{"a":"Taco","b":"1F32E","j":["mexican","food"]},"burrito":{"a":"Burrito","b":"1F32F","j":["mexican","wrap","food"]},"tamale":{"a":"Tamale","b":"1FAD4","j":["mexican","wrapped","food","masa"]},"stuffed-flatbread":{"a":"Stuffed Flatbread","b":"1F959","j":["falafel","flatbread","food","gyro","kebab","stuffed","mediterranean"]},"falafel":{"a":"Falafel","b":"1F9C6","j":["chickpea","meatball","food","mediterranean"]},"egg":{"a":"Egg","b":"1F95A","j":["breakfast","food","chicken"]},"cooking":{"a":"Cooking","b":"1F373","j":["breakfast","egg","frying","pan","food","kitchen","skillet"]},"shallow-pan-of-food":{"a":"Shallow Pan of Food","b":"1F958","j":["casserole","food","paella","pan","shallow","cooking","skillet"]},"pot-of-food":{"a":"Pot of Food","b":"1F372","j":["pot","stew","food","meat","soup","hot pot"]},"fondue":{"a":"Fondue","b":"1FAD5","j":["cheese","chocolate","melted","pot","Swiss","food"]},"bowl-with-spoon":{"a":"Bowl with Spoon","b":"1F963","j":["breakfast","cereal","congee","oatmeal","porridge","food"]},"green-salad":{"a":"Green Salad","b":"1F957","j":["food","green","salad","healthy","lettuce","vegetable"]},"popcorn":{"a":"Popcorn","b":"1F37F","j":["food","movie theater","films","snack","drama"]},"butter":{"a":"Butter","b":"1F9C8","j":["dairy","food","cook"]},"salt":{"a":"Salt","b":"1F9C2","j":["condiment","shaker"]},"canned-food":{"a":"Canned Food","b":"1F96B","j":["can","food","soup","tomatoes"]},"bento-box":{"a":"Bento Box","b":"1F371","j":["bento","box","food","japanese","lunch"]},"rice-cracker":{"a":"Rice Cracker","b":"1F358","j":["cracker","rice","food","japanese","snack"]},"rice-ball":{"a":"Rice Ball","b":"1F359","j":["ball","Japanese","rice","food","japanese"]},"cooked-rice":{"a":"Cooked Rice","b":"1F35A","j":["cooked","rice","food","asian"]},"curry-rice":{"a":"Curry Rice","b":"1F35B","j":["curry","rice","food","spicy","hot","indian"]},"steaming-bowl":{"a":"Steaming Bowl","b":"1F35C","j":["bowl","noodle","ramen","steaming","food","japanese","chopsticks"]},"spaghetti":{"a":"Spaghetti","b":"1F35D","j":["pasta","food","italian","noodle"]},"roasted-sweet-potato":{"a":"Roasted Sweet Potato","b":"1F360","j":["potato","roasted","sweet","food","nature","plant"]},"oden":{"a":"Oden","b":"1F362","j":["kebab","seafood","skewer","stick","food","japanese"]},"sushi":{"a":"Sushi","b":"1F363","j":["food","fish","japanese","rice"]},"fried-shrimp":{"a":"Fried Shrimp","b":"1F364","j":["fried","prawn","shrimp","tempura","food","animal","appetizer","summer"]},"fish-cake-with-swirl":{"a":"Fish Cake with Swirl","b":"1F365","j":["cake","fish","pastry","swirl","food","japan","sea","beach","narutomaki","pink","kamaboko","surimi","ramen"]},"moon-cake":{"a":"Moon Cake","b":"1F96E","j":["autumn","festival","yuèbǐng","food","dessert"]},"dango":{"a":"Dango","b":"1F361","j":["dessert","Japanese","skewer","stick","sweet","food","japanese","barbecue","meat"]},"dumpling":{"a":"Dumpling","b":"1F95F","j":["empanada","gyōza","jiaozi","pierogi","potsticker","food","gyoza"]},"fortune-cookie":{"a":"Fortune Cookie","b":"1F960","j":["prophecy","food","dessert"]},"takeout-box":{"a":"Takeout Box","b":"1F961","j":["oyster pail","food","leftovers"]},"crab":{"a":"Crab","b":"1F980","j":["Cancer","zodiac","animal","crustacean"]},"lobster":{"a":"Lobster","b":"1F99E","j":["bisque","claws","seafood","animal","nature"]},"shrimp":{"a":"Shrimp","b":"1F990","j":["food","shellfish","small","animal","ocean","nature","seafood"]},"squid":{"a":"Squid","b":"1F991","j":["food","molusc","animal","nature","ocean","sea"]},"oyster":{"a":"Oyster","b":"1F9AA","j":["diving","pearl","food"]},"soft-ice-cream":{"a":"Soft Ice Cream","b":"1F366","j":["cream","dessert","ice","icecream","soft","sweet","food","hot","summer"]},"shaved-ice":{"a":"Shaved Ice","b":"1F367","j":["dessert","ice","shaved","sweet","hot","summer"]},"ice-cream":{"a":"Ice Cream","b":"1F368","j":["cream","dessert","ice","sweet","food","hot"]},"doughnut":{"a":"Doughnut","b":"1F369","j":["breakfast","dessert","donut","sweet","food","snack"]},"cookie":{"a":"Cookie","b":"1F36A","j":["dessert","sweet","food","snack","oreo","chocolate"]},"birthday-cake":{"a":"Birthday Cake","b":"1F382","j":["birthday","cake","celebration","dessert","pastry","sweet","food"]},"shortcake":{"a":"Shortcake","b":"1F370","j":["cake","dessert","pastry","slice","sweet","food"]},"cupcake":{"a":"Cupcake","b":"1F9C1","j":["bakery","sweet","food","dessert"]},"pie":{"a":"Pie","b":"1F967","j":["filling","pastry","fruit","meat","food","dessert"]},"chocolate-bar":{"a":"Chocolate Bar","b":"1F36B","j":["bar","chocolate","dessert","sweet","food","snack"]},"candy":{"a":"Candy","b":"1F36C","j":["dessert","sweet","snack","lolly"]},"lollipop":{"a":"Lollipop","b":"1F36D","j":["candy","dessert","sweet","food","snack"]},"custard":{"a":"Custard","b":"1F36E","j":["dessert","pudding","sweet","food"]},"honey-pot":{"a":"Honey Pot","b":"1F36F","j":["honey","honeypot","pot","sweet","bees","kitchen"]},"baby-bottle":{"a":"Baby Bottle","b":"1F37C","j":["baby","bottle","drink","milk","food","container"]},"glass-of-milk":{"a":"Glass of Milk","b":"1F95B","j":["drink","glass","milk","beverage","cow"]},"hot-beverage":{"a":"Hot Beverage","b":"2615-FE0F","j":["beverage","coffee","drink","hot","steaming","tea","caffeine","latte","espresso","mug"]},"teapot":{"a":"Teapot","b":"1FAD6","j":["drink","pot","tea","hot"]},"teacup-without-handle":{"a":"Teacup Without Handle","b":"1F375","j":["beverage","cup","drink","tea","teacup","bowl","breakfast","green","british"]},"sake":{"a":"Sake","b":"1F376","j":["bar","beverage","bottle","cup","drink","wine","drunk","japanese","alcohol","booze"]},"bottle-with-popping-cork":{"a":"Bottle with Popping Cork","b":"1F37E","j":["bar","bottle","cork","drink","popping","wine","celebration"]},"wine-glass":{"a":"Wine Glass","b":"1F377","j":["bar","beverage","drink","glass","wine","drunk","alcohol","booze"]},"cocktail-glass":{"a":"Cocktail Glass","b":"1F378-FE0F","j":["bar","cocktail","drink","glass","drunk","alcohol","beverage","booze","mojito"]},"tropical-drink":{"a":"Tropical Drink","b":"1F379","j":["bar","drink","tropical","beverage","cocktail","summer","beach","alcohol","booze","mojito"]},"beer-mug":{"a":"Beer Mug","b":"1F37A","j":["bar","beer","drink","mug","relax","beverage","drunk","party","pub","summer","alcohol","booze"]},"clinking-beer-mugs":{"a":"Clinking Beer Mugs","b":"1F37B","j":["bar","beer","clink","drink","mug","relax","beverage","drunk","party","pub","summer","alcohol","booze"]},"clinking-glasses":{"a":"Clinking Glasses","b":"1F942","j":["celebrate","clink","drink","glass","beverage","party","alcohol","cheers","wine","champagne","toast"]},"tumbler-glass":{"a":"Tumbler Glass","b":"1F943","j":["glass","liquor","shot","tumbler","whisky","drink","beverage","drunk","alcohol","booze","bourbon","scotch"]},"pouring-liquid":{"a":"Pouring Liquid","b":"1FAD7","j":["drink","empty","glass","spill","cup","water"]},"cup-with-straw":{"a":"Cup with Straw","b":"1F964","j":["juice","soda","malt","soft drink","water","drink"]},"bubble-tea":{"a":"Bubble Tea","b":"1F9CB","j":["bubble","milk","pearl","tea","taiwan","boba","milk tea","straw"]},"beverage-box":{"a":"Beverage Box","b":"1F9C3","j":["beverage","box","juice","straw","sweet","drink"]},"mate":{"a":"Mate","b":"1F9C9","j":["drink","tea","beverage"]},"ice":{"a":"Ice","b":"1F9CA","j":["cold","ice cube","iceberg","water"]},"chopsticks":{"a":"Chopsticks","b":"1F962","j":["hashi","jeotgarak","kuaizi","food"]},"fork-and-knife-with-plate":{"a":"Fork and Knife with Plate","b":"1F37D-FE0F","j":["cooking","fork","knife","plate","food","eat","meal","lunch","dinner","restaurant"]},"fork-and-knife":{"a":"Fork and Knife","b":"1F374","j":["cooking","cutlery","fork","knife","kitchen"]},"spoon":{"a":"Spoon","b":"1F944","j":["tableware","cutlery","kitchen"]},"kitchen-knife":{"a":"Kitchen Knife","b":"1F52A","j":["cooking","hocho","knife","tool","weapon","blade","cutlery","kitchen"]},"jar":{"a":"Jar","b":"1FAD9","j":["condiment","container","empty","sauce","store"]},"amphora":{"a":"Amphora","b":"1F3FA","j":["Aquarius","cooking","drink","jug","zodiac","vase","jar"]},"globe-showing-europeafrica":{"a":"Globe Showing Europe-Africa","b":"1F30D-FE0F","j":["Africa","earth","Europe","globe","globe showing Europe-Africa","world","globe_showing_europe_africa","international"]},"globe-showing-americas":{"a":"Globe Showing Americas","b":"1F30E-FE0F","j":["Americas","earth","globe","globe showing Americas","world","USA","international"]},"globe-showing-asiaaustralia":{"a":"Globe Showing Asia-Australia","b":"1F30F-FE0F","j":["Asia","Australia","earth","globe","globe showing Asia-Australia","world","globe_showing_asia_australia","east","international"]},"globe-with-meridians":{"a":"Globe with Meridians","b":"1F310","j":["earth","globe","meridians","world","international","internet","interweb","i18n"]},"world-map":{"a":"World Map","b":"1F5FA-FE0F","j":["map","world","location","direction"]},"map-of-japan":{"a":"Map of Japan","b":"1F5FE","j":["Japan","map","map of Japan","nation","country","japanese","asia"]},"compass":{"a":"Compass","b":"1F9ED","j":["magnetic","navigation","orienteering"]},"snowcapped-mountain":{"a":"Snow-Capped Mountain","b":"1F3D4-FE0F","j":["cold","mountain","snow","snow-capped mountain","snow_capped_mountain","photo","nature","environment","winter"]},"mountain":{"a":"Mountain","b":"26F0-FE0F","j":["photo","nature","environment"]},"volcano":{"a":"Volcano","b":"1F30B","j":["eruption","mountain","photo","nature","disaster"]},"mount-fuji":{"a":"Mount Fuji","b":"1F5FB","j":["fuji","mountain","photo","nature","japanese"]},"camping":{"a":"Camping","b":"1F3D5-FE0F","j":["photo","outdoors","tent"]},"beach-with-umbrella":{"a":"Beach with Umbrella","b":"1F3D6-FE0F","j":["beach","umbrella","weather","summer","sunny","sand","mojito"]},"desert":{"a":"Desert","b":"1F3DC-FE0F","j":["photo","warm","saharah"]},"desert-island":{"a":"Desert Island","b":"1F3DD-FE0F","j":["desert","island","photo","tropical","mojito"]},"national-park":{"a":"National Park","b":"1F3DE-FE0F","j":["park","photo","environment","nature"]},"stadium":{"a":"Stadium","b":"1F3DF-FE0F","j":["photo","place","sports","concert","venue"]},"classical-building":{"a":"Classical Building","b":"1F3DB-FE0F","j":["classical","art","culture","history"]},"building-construction":{"a":"Building Construction","b":"1F3D7-FE0F","j":["construction","wip","working","progress"]},"brick":{"a":"Brick","b":"1F9F1","j":["bricks","clay","mortar","wall"]},"rock":{"a":"Rock","b":"1FAA8","j":["boulder","heavy","solid","stone"]},"wood":{"a":"Wood","b":"1FAB5","j":["log","lumber","timber","nature","trunk"]},"hut":{"a":"Hut","b":"1F6D6","j":["house","roundhouse","yurt","structure"]},"houses":{"a":"Houses","b":"1F3D8-FE0F","j":["buildings","photo"]},"derelict-house":{"a":"Derelict House","b":"1F3DA-FE0F","j":["derelict","house","abandon","evict","broken","building"]},"house":{"a":"House","b":"1F3E0-FE0F","j":["home","building"]},"house-with-garden":{"a":"House with Garden","b":"1F3E1","j":["garden","home","house","plant","nature"]},"office-building":{"a":"Office Building","b":"1F3E2","j":["building","bureau","work"]},"japanese-post-office":{"a":"Japanese Post Office","b":"1F3E3","j":["Japanese","Japanese post office","post","building","envelope","communication"]},"post-office":{"a":"Post Office","b":"1F3E4","j":["European","post","building","email"]},"hospital":{"a":"Hospital","b":"1F3E5","j":["doctor","medicine","building","health","surgery"]},"bank":{"a":"Bank","b":"1F3E6","j":["building","money","sales","cash","business","enterprise"]},"hotel":{"a":"Hotel","b":"1F3E8","j":["building","accomodation","checkin"]},"love-hotel":{"a":"Love Hotel","b":"1F3E9","j":["hotel","love","like","affection","dating"]},"convenience-store":{"a":"Convenience Store","b":"1F3EA","j":["convenience","store","building","shopping","groceries"]},"school":{"a":"School","b":"1F3EB","j":["building","student","education","learn","teach"]},"department-store":{"a":"Department Store","b":"1F3EC","j":["department","store","building","shopping","mall"]},"factory":{"a":"Factory","b":"1F3ED-FE0F","j":["building","industry","pollution","smoke"]},"japanese-castle":{"a":"Japanese Castle","b":"1F3EF","j":["castle","Japanese","photo","building"]},"castle":{"a":"Castle","b":"1F3F0","j":["European","building","royalty","history"]},"wedding":{"a":"Wedding","b":"1F492","j":["chapel","romance","love","like","affection","couple","marriage","bride","groom"]},"tokyo-tower":{"a":"Tokyo Tower","b":"1F5FC","j":["Tokyo","tower","photo","japanese"]},"statue-of-liberty":{"a":"Statue of Liberty","b":"1F5FD","j":["liberty","statue","american","newyork"]},"church":{"a":"Church","b":"26EA-FE0F","j":["Christian","cross","religion","building","christ"]},"mosque":{"a":"Mosque","b":"1F54C","j":["islam","Muslim","religion","worship","minaret"]},"hindu-temple":{"a":"Hindu Temple","b":"1F6D5","j":["hindu","temple","religion"]},"synagogue":{"a":"Synagogue","b":"1F54D","j":["Jew","Jewish","religion","temple","judaism","worship","jewish"]},"shinto-shrine":{"a":"Shinto Shrine","b":"26E9-FE0F","j":["religion","shinto","shrine","temple","japan","kyoto"]},"kaaba":{"a":"Kaaba","b":"1F54B","j":["islam","Muslim","religion","mecca","mosque"]},"fountain":{"a":"Fountain","b":"26F2-FE0F","j":["photo","summer","water","fresh"]},"tent":{"a":"Tent","b":"26FA-FE0F","j":["camping","photo","outdoors"]},"foggy":{"a":"Foggy","b":"1F301","j":["fog","photo","mountain"]},"night-with-stars":{"a":"Night with Stars","b":"1F303","j":["night","star","evening","city","downtown"]},"cityscape":{"a":"Cityscape","b":"1F3D9-FE0F","j":["city","photo","night life","urban"]},"sunrise-over-mountains":{"a":"Sunrise over Mountains","b":"1F304","j":["morning","mountain","sun","sunrise","view","vacation","photo"]},"sunrise":{"a":"Sunrise","b":"1F305","j":["morning","sun","view","vacation","photo"]},"cityscape-at-dusk":{"a":"Cityscape at Dusk","b":"1F306","j":["city","dusk","evening","landscape","sunset","photo","sky","buildings"]},"sunset":{"a":"Sunset","b":"1F307","j":["dusk","sun","photo","good morning","dawn"]},"bridge-at-night":{"a":"Bridge at Night","b":"1F309","j":["bridge","night","photo","sanfrancisco"]},"hot-springs":{"a":"Hot Springs","b":"2668-FE0F","j":["hot","hotsprings","springs","steaming","bath","warm","relax"]},"carousel-horse":{"a":"Carousel Horse","b":"1F3A0","j":["carousel","horse","photo","carnival"]},"playground-slide":{"a":"Playground Slide","b":"1F6DD","j":["amusement park","play","fun","park"]},"ferris-wheel":{"a":"Ferris Wheel","b":"1F3A1","j":["amusement park","ferris","wheel","photo","carnival","londoneye"]},"roller-coaster":{"a":"Roller Coaster","b":"1F3A2","j":["amusement park","coaster","roller","carnival","playground","photo","fun"]},"barber-pole":{"a":"Barber Pole","b":"1F488","j":["barber","haircut","pole","hair","salon","style"]},"circus-tent":{"a":"Circus Tent","b":"1F3AA","j":["circus","tent","festival","carnival","party"]},"locomotive":{"a":"Locomotive","b":"1F682","j":["engine","railway","steam","train","transportation","vehicle"]},"railway-car":{"a":"Railway Car","b":"1F683","j":["car","electric","railway","train","tram","trolleybus","transportation","vehicle"]},"highspeed-train":{"a":"High-Speed Train","b":"1F684","j":["high-speed train","railway","shinkansen","speed","train","high_speed_train","transportation","vehicle"]},"bullet-train":{"a":"Bullet Train","b":"1F685","j":["bullet","railway","shinkansen","speed","train","transportation","vehicle","fast","public","travel"]},"train":{"a":"Train","b":"1F686","j":["railway","transportation","vehicle"]},"metro":{"a":"Metro","b":"1F687-FE0F","j":["subway","transportation","blue-square","mrt","underground","tube"]},"light-rail":{"a":"Light Rail","b":"1F688","j":["railway","transportation","vehicle"]},"station":{"a":"Station","b":"1F689","j":["railway","train","transportation","vehicle","public"]},"tram":{"a":"Tram","b":"1F68A","j":["trolleybus","transportation","vehicle"]},"monorail":{"a":"Monorail","b":"1F69D","j":["vehicle","transportation"]},"mountain-railway":{"a":"Mountain Railway","b":"1F69E","j":["car","mountain","railway","transportation","vehicle"]},"tram-car":{"a":"Tram Car","b":"1F68B","j":["car","tram","trolleybus","transportation","vehicle","carriage","public","travel"]},"bus":{"a":"Bus","b":"1F68C","j":["vehicle","car","transportation"]},"oncoming-bus":{"a":"Oncoming Bus","b":"1F68D-FE0F","j":["bus","oncoming","vehicle","transportation"]},"trolleybus":{"a":"Trolleybus","b":"1F68E","j":["bus","tram","trolley","bart","transportation","vehicle"]},"minibus":{"a":"Minibus","b":"1F690","j":["bus","vehicle","car","transportation"]},"ambulance":{"a":"Ambulance","b":"1F691-FE0F","j":["vehicle","health","911","hospital"]},"fire-engine":{"a":"Fire Engine","b":"1F692","j":["engine","fire","truck","transportation","cars","vehicle"]},"police-car":{"a":"Police Car","b":"1F693","j":["car","patrol","police","vehicle","cars","transportation","law","legal","enforcement"]},"oncoming-police-car":{"a":"Oncoming Police Car","b":"1F694-FE0F","j":["car","oncoming","police","vehicle","law","legal","enforcement","911"]},"taxi":{"a":"Taxi","b":"1F695","j":["vehicle","uber","cars","transportation"]},"oncoming-taxi":{"a":"Oncoming Taxi","b":"1F696","j":["oncoming","taxi","vehicle","cars","uber"]},"automobile":{"a":"Automobile","b":"1F697","j":["car","red","transportation","vehicle"]},"oncoming-automobile":{"a":"Oncoming Automobile","b":"1F698-FE0F","j":["automobile","car","oncoming","vehicle","transportation"]},"sport-utility-vehicle":{"a":"Sport Utility Vehicle","b":"1F699","j":["recreational","sport utility","transportation","vehicle"]},"pickup-truck":{"a":"Pickup Truck","b":"1F6FB","j":["pick-up","pickup","truck","car","transportation"]},"delivery-truck":{"a":"Delivery Truck","b":"1F69A","j":["delivery","truck","cars","transportation"]},"articulated-lorry":{"a":"Articulated Lorry","b":"1F69B","j":["lorry","semi","truck","vehicle","cars","transportation","express"]},"tractor":{"a":"Tractor","b":"1F69C","j":["vehicle","car","farming","agriculture"]},"racing-car":{"a":"Racing Car","b":"1F3CE-FE0F","j":["car","racing","sports","race","fast","formula","f1"]},"motorcycle":{"a":"Motorcycle","b":"1F3CD-FE0F","j":["racing","race","sports","fast"]},"motor-scooter":{"a":"Motor Scooter","b":"1F6F5","j":["motor","scooter","vehicle","vespa","sasha"]},"manual-wheelchair":{"a":"Manual Wheelchair","b":"1F9BD","j":["accessibility"]},"motorized-wheelchair":{"a":"Motorized Wheelchair","b":"1F9BC","j":["accessibility"]},"auto-rickshaw":{"a":"Auto Rickshaw","b":"1F6FA","j":["tuk tuk","move","transportation"]},"bicycle":{"a":"Bicycle","b":"1F6B2-FE0F","j":["bike","sports","exercise","hipster"]},"kick-scooter":{"a":"Kick Scooter","b":"1F6F4","j":["kick","scooter","vehicle","razor"]},"skateboard":{"a":"Skateboard","b":"1F6F9","j":["board"]},"roller-skate":{"a":"Roller Skate","b":"1F6FC","j":["roller","skate","footwear","sports"]},"bus-stop":{"a":"Bus Stop","b":"1F68F","j":["bus","stop","transportation","wait"]},"motorway":{"a":"Motorway","b":"1F6E3-FE0F","j":["highway","road","cupertino","interstate"]},"railway-track":{"a":"Railway Track","b":"1F6E4-FE0F","j":["railway","train","transportation"]},"oil-drum":{"a":"Oil Drum","b":"1F6E2-FE0F","j":["drum","oil","barrell"]},"fuel-pump":{"a":"Fuel Pump","b":"26FD-FE0F","j":["diesel","fuel","fuelpump","gas","pump","station","gas station","petroleum"]},"wheel":{"a":"Wheel","b":"1F6DE","j":["circle","tire","turn","car","transport"]},"police-car-light":{"a":"Police Car Light","b":"1F6A8","j":["beacon","car","light","police","revolving","ambulance","911","emergency","alert","error","pinged","law","legal"]},"horizontal-traffic-light":{"a":"Horizontal Traffic Light","b":"1F6A5","j":["light","signal","traffic","transportation"]},"vertical-traffic-light":{"a":"Vertical Traffic Light","b":"1F6A6","j":["light","signal","traffic","transportation","driving"]},"stop-sign":{"a":"Stop Sign","b":"1F6D1","j":["octagonal","sign","stop"]},"construction":{"a":"Construction","b":"1F6A7","j":["barrier","wip","progress","caution","warning"]},"anchor":{"a":"Anchor","b":"2693-FE0F","j":["ship","tool","ferry","sea","boat"]},"ring-buoy":{"a":"Ring Buoy","b":"1F6DF","j":["float","life preserver","life saver","rescue","safety"]},"sailboat":{"a":"Sailboat","b":"26F5-FE0F","j":["boat","resort","sea","yacht","ship","summer","transportation","water","sailing"]},"canoe":{"a":"Canoe","b":"1F6F6","j":["boat","paddle","water","ship"]},"speedboat":{"a":"Speedboat","b":"1F6A4","j":["boat","ship","transportation","vehicle","summer"]},"passenger-ship":{"a":"Passenger Ship","b":"1F6F3-FE0F","j":["passenger","ship","yacht","cruise","ferry"]},"ferry":{"a":"Ferry","b":"26F4-FE0F","j":["boat","passenger","ship","yacht"]},"motor-boat":{"a":"Motor Boat","b":"1F6E5-FE0F","j":["boat","motorboat","ship"]},"ship":{"a":"Ship","b":"1F6A2","j":["boat","passenger","transportation","titanic","deploy"]},"airplane":{"a":"Airplane","b":"2708-FE0F","j":["aeroplane","vehicle","transportation","flight","fly"]},"small-airplane":{"a":"Small Airplane","b":"1F6E9-FE0F","j":["aeroplane","airplane","flight","transportation","fly","vehicle"]},"airplane-departure":{"a":"Airplane Departure","b":"1F6EB","j":["aeroplane","airplane","check-in","departure","departures","airport","flight","landing"]},"airplane-arrival":{"a":"Airplane Arrival","b":"1F6EC","j":["aeroplane","airplane","arrivals","arriving","landing","airport","flight","boarding"]},"parachute":{"a":"Parachute","b":"1FA82","j":["hang-glide","parasail","skydive","fly","glide"]},"seat":{"a":"Seat","b":"1F4BA","j":["chair","sit","airplane","transport","bus","flight","fly"]},"helicopter":{"a":"Helicopter","b":"1F681","j":["vehicle","transportation","fly"]},"suspension-railway":{"a":"Suspension Railway","b":"1F69F","j":["railway","suspension","vehicle","transportation"]},"mountain-cableway":{"a":"Mountain Cableway","b":"1F6A0","j":["cable","gondola","mountain","transportation","vehicle","ski"]},"aerial-tramway":{"a":"Aerial Tramway","b":"1F6A1","j":["aerial","cable","car","gondola","tramway","transportation","vehicle","ski"]},"satellite":{"a":"Satellite","b":"1F6F0-FE0F","j":["space","communication","gps","orbit","spaceflight","NASA","ISS"]},"rocket":{"a":"Rocket","b":"1F680","j":["space","launch","ship","staffmode","NASA","outer space","outer_space","fly"]},"flying-saucer":{"a":"Flying Saucer","b":"1F6F8","j":["UFO","transportation","vehicle","ufo"]},"bellhop-bell":{"a":"Bellhop Bell","b":"1F6CE-FE0F","j":["bell","bellhop","hotel","service"]},"luggage":{"a":"Luggage","b":"1F9F3","j":["packing","travel"]},"hourglass-done":{"a":"Hourglass Done","b":"231B-FE0F","j":["sand","timer","time","clock","oldschool","limit","exam","quiz","test"]},"hourglass-not-done":{"a":"Hourglass Not Done","b":"23F3-FE0F","j":["hourglass","sand","timer","oldschool","time","countdown"]},"watch":{"a":"Watch","b":"231A-FE0F","j":["clock","time","accessories"]},"alarm-clock":{"a":"Alarm Clock","b":"23F0","j":["alarm","clock","time","wake"]},"stopwatch":{"a":"Stopwatch","b":"23F1-FE0F","j":["clock","time","deadline"]},"timer-clock":{"a":"Timer Clock","b":"23F2-FE0F","j":["clock","timer","alarm"]},"mantelpiece-clock":{"a":"Mantelpiece Clock","b":"1F570-FE0F","j":["clock","time"]},"twelve-oclock":{"a":"Twelve O’Clock","b":"1F55B-FE0F","j":["00","12","12:00","clock","o’clock","twelve","twelve_o_clock","00:00","0000","1200","time","noon","midnight","midday","late","early","schedule"]},"twelvethirty":{"a":"Twelve-Thirty","b":"1F567-FE0F","j":["12","12:30","clock","thirty","twelve","twelve-thirty","twelve_thirty","00:30","0030","1230","time","late","early","schedule"]},"one-oclock":{"a":"One O’Clock","b":"1F550-FE0F","j":["00","1","1:00","clock","o’clock","one","one_o_clock","100","13:00","1300","time","late","early","schedule"]},"onethirty":{"a":"One-Thirty","b":"1F55C-FE0F","j":["1","1:30","clock","one","one-thirty","thirty","one_thirty","130","13:30","1330","time","late","early","schedule"]},"two-oclock":{"a":"Two O’Clock","b":"1F551-FE0F","j":["00","2","2:00","clock","o’clock","two","two_o_clock","200","14:00","1400","time","late","early","schedule"]},"twothirty":{"a":"Two-Thirty","b":"1F55D-FE0F","j":["2","2:30","clock","thirty","two","two-thirty","two_thirty","230","14:30","1430","time","late","early","schedule"]},"three-oclock":{"a":"Three O’Clock","b":"1F552-FE0F","j":["00","3","3:00","clock","o’clock","three","three_o_clock","300","15:00","1500","time","late","early","schedule"]},"threethirty":{"a":"Three-Thirty","b":"1F55E-FE0F","j":["3","3:30","clock","thirty","three","three-thirty","three_thirty","330","15:30","1530","time","late","early","schedule"]},"four-oclock":{"a":"Four O’Clock","b":"1F553-FE0F","j":["00","4","4:00","clock","four","o’clock","four_o_clock","400","16:00","1600","time","late","early","schedule"]},"fourthirty":{"a":"Four-Thirty","b":"1F55F-FE0F","j":["4","4:30","clock","four","four-thirty","thirty","four_thirty","430","16:30","1630","time","late","early","schedule"]},"five-oclock":{"a":"Five O’Clock","b":"1F554-FE0F","j":["00","5","5:00","clock","five","o’clock","five_o_clock","500","17:00","1700","time","late","early","schedule"]},"fivethirty":{"a":"Five-Thirty","b":"1F560-FE0F","j":["5","5:30","clock","five","five-thirty","thirty","five_thirty","530","17:30","1730","time","late","early","schedule"]},"six-oclock":{"a":"Six O’Clock","b":"1F555-FE0F","j":["00","6","6:00","clock","o’clock","six","six_o_clock","600","18:00","1800","time","late","early","schedule","dawn","dusk"]},"sixthirty":{"a":"Six-Thirty","b":"1F561-FE0F","j":["6","6:30","clock","six","six-thirty","thirty","six_thirty","630","18:30","1830","time","late","early","schedule"]},"seven-oclock":{"a":"Seven O’Clock","b":"1F556-FE0F","j":["00","7","7:00","clock","o’clock","seven","seven_o_clock","700","19:00","1900","time","late","early","schedule"]},"seventhirty":{"a":"Seven-Thirty","b":"1F562-FE0F","j":["7","7:30","clock","seven","seven-thirty","thirty","seven_thirty","730","19:30","1930","time","late","early","schedule"]},"eight-oclock":{"a":"Eight O’Clock","b":"1F557-FE0F","j":["00","8","8:00","clock","eight","o’clock","eight_o_clock","800","20:00","2000","time","late","early","schedule"]},"eightthirty":{"a":"Eight-Thirty","b":"1F563-FE0F","j":["8","8:30","clock","eight","eight-thirty","thirty","eight_thirty","830","20:30","2030","time","late","early","schedule"]},"nine-oclock":{"a":"Nine O’Clock","b":"1F558-FE0F","j":["00","9","9:00","clock","nine","o’clock","nine_o_clock","900","21:00","2100","time","late","early","schedule"]},"ninethirty":{"a":"Nine-Thirty","b":"1F564-FE0F","j":["9","9:30","clock","nine","nine-thirty","thirty","nine_thirty","930","21:30","2130","time","late","early","schedule"]},"ten-oclock":{"a":"Ten O’Clock","b":"1F559-FE0F","j":["00","10","10:00","clock","o’clock","ten","ten_o_clock","1000","22:00","2200","time","late","early","schedule"]},"tenthirty":{"a":"Ten-Thirty","b":"1F565-FE0F","j":["10","10:30","clock","ten","ten-thirty","thirty","ten_thirty","1030","22:30","2230","time","late","early","schedule"]},"eleven-oclock":{"a":"Eleven O’Clock","b":"1F55A-FE0F","j":["00","11","11:00","clock","eleven","o’clock","eleven_o_clock","1100","23:00","2300","time","late","early","schedule"]},"eleventhirty":{"a":"Eleven-Thirty","b":"1F566-FE0F","j":["11","11:30","clock","eleven","eleven-thirty","thirty","eleven_thirty","1130","23:30","2330","time","late","early","schedule"]},"new-moon":{"a":"New Moon","b":"1F311","j":["dark","moon","nature","twilight","planet","space","night","evening","sleep"]},"waxing-crescent-moon":{"a":"Waxing Crescent Moon","b":"1F312","j":["crescent","moon","waxing","nature","twilight","planet","space","night","evening","sleep"]},"first-quarter-moon":{"a":"First Quarter Moon","b":"1F313","j":["moon","quarter","nature","twilight","planet","space","night","evening","sleep"]},"waxing-gibbous-moon":{"a":"Waxing Gibbous Moon","b":"1F314","j":["gibbous","moon","waxing","nature","night","sky","gray","twilight","planet","space","evening","sleep"]},"full-moon":{"a":"Full Moon","b":"1F315-FE0F","j":["full","moon","nature","yellow","twilight","planet","space","night","evening","sleep"]},"waning-gibbous-moon":{"a":"Waning Gibbous Moon","b":"1F316","j":["gibbous","moon","waning","nature","twilight","planet","space","night","evening","sleep","waxing_gibbous_moon"]},"last-quarter-moon":{"a":"Last Quarter Moon","b":"1F317","j":["moon","quarter","nature","twilight","planet","space","night","evening","sleep"]},"waning-crescent-moon":{"a":"Waning Crescent Moon","b":"1F318","j":["crescent","moon","waning","nature","twilight","planet","space","night","evening","sleep"]},"crescent-moon":{"a":"Crescent Moon","b":"1F319","j":["crescent","moon","night","sleep","sky","evening","magic"]},"new-moon-face":{"a":"New Moon Face","b":"1F31A","j":["face","moon","nature","twilight","planet","space","night","evening","sleep"]},"first-quarter-moon-face":{"a":"First Quarter Moon Face","b":"1F31B","j":["face","moon","quarter","nature","twilight","planet","space","night","evening","sleep"]},"last-quarter-moon-face":{"a":"Last Quarter Moon Face","b":"1F31C-FE0F","j":["face","moon","quarter","nature","twilight","planet","space","night","evening","sleep"]},"thermometer":{"a":"Thermometer","b":"1F321-FE0F","j":["weather","temperature","hot","cold"]},"sun":{"a":"Sun","b":"2600-FE0F","j":["bright","rays","sunny","weather","nature","brightness","summer","beach","spring"]},"full-moon-face":{"a":"Full Moon Face","b":"1F31D","j":["bright","face","full","moon","nature","twilight","planet","space","night","evening","sleep"]},"sun-with-face":{"a":"Sun with Face","b":"1F31E","j":["bright","face","sun","nature","morning","sky"]},"ringed-planet":{"a":"Ringed Planet","b":"1FA90","j":["saturn","saturnine","outerspace"]},"star":{"a":"Star","b":"2B50-FE0F","j":["night","yellow"]},"glowing-star":{"a":"Glowing Star","b":"1F31F","j":["glittery","glow","shining","sparkle","star","night","awesome","good","magic"]},"shooting-star":{"a":"Shooting Star","b":"1F320","j":["falling","shooting","star","night","photo"]},"milky-way":{"a":"Milky Way","b":"1F30C","j":["space","photo","stars"]},"cloud":{"a":"Cloud","b":"2601-FE0F","j":["weather","sky"]},"sun-behind-cloud":{"a":"Sun Behind Cloud","b":"26C5-FE0F","j":["cloud","sun","weather","nature","cloudy","morning","fall","spring"]},"cloud-with-lightning-and-rain":{"a":"Cloud with Lightning and Rain","b":"26C8-FE0F","j":["cloud","rain","thunder","weather","lightning"]},"sun-behind-small-cloud":{"a":"Sun Behind Small Cloud","b":"1F324-FE0F","j":["cloud","sun","weather"]},"sun-behind-large-cloud":{"a":"Sun Behind Large Cloud","b":"1F325-FE0F","j":["cloud","sun","weather"]},"sun-behind-rain-cloud":{"a":"Sun Behind Rain Cloud","b":"1F326-FE0F","j":["cloud","rain","sun","weather"]},"cloud-with-rain":{"a":"Cloud with Rain","b":"1F327-FE0F","j":["cloud","rain","weather"]},"cloud-with-snow":{"a":"Cloud with Snow","b":"1F328-FE0F","j":["cloud","cold","snow","weather"]},"cloud-with-lightning":{"a":"Cloud with Lightning","b":"1F329-FE0F","j":["cloud","lightning","weather","thunder"]},"tornado":{"a":"Tornado","b":"1F32A-FE0F","j":["cloud","whirlwind","weather","cyclone","twister"]},"fog":{"a":"Fog","b":"1F32B-FE0F","j":["cloud","weather"]},"wind-face":{"a":"Wind Face","b":"1F32C-FE0F","j":["blow","cloud","face","wind","gust","air"]},"cyclone":{"a":"Cyclone","b":"1F300","j":["dizzy","hurricane","twister","typhoon","weather","swirl","blue","cloud","vortex","spiral","whirlpool","spin","tornado"]},"rainbow":{"a":"Rainbow","b":"1F308","j":["rain","nature","happy","unicorn_face","photo","sky","spring"]},"closed-umbrella":{"a":"Closed Umbrella","b":"1F302","j":["clothing","rain","umbrella","weather","drizzle"]},"umbrella":{"a":"Umbrella","b":"2602-FE0F","j":["clothing","rain","weather","spring"]},"umbrella-with-rain-drops":{"a":"Umbrella with Rain Drops","b":"2614-FE0F","j":["clothing","drop","rain","umbrella","rainy","weather","spring"]},"umbrella-on-ground":{"a":"Umbrella on Ground","b":"26F1-FE0F","j":["rain","sun","umbrella","weather","summer"]},"high-voltage":{"a":"High Voltage","b":"26A1-FE0F","j":["danger","electric","lightning","voltage","zap","thunder","weather","lightning bolt","fast"]},"snowflake":{"a":"Snowflake","b":"2744-FE0F","j":["cold","snow","winter","season","weather","christmas","xmas"]},"snowman":{"a":"Snowman","b":"2603-FE0F","j":["cold","snow","winter","season","weather","christmas","xmas","frozen"]},"snowman-without-snow":{"a":"Snowman Without Snow","b":"26C4-FE0F","j":["cold","snow","snowman","winter","season","weather","christmas","xmas","frozen","without_snow"]},"comet":{"a":"Comet","b":"2604-FE0F","j":["space"]},"fire":{"a":"Fire","b":"1F525","j":["flame","tool","hot","cook"]},"droplet":{"a":"Droplet","b":"1F4A7","j":["cold","comic","drop","sweat","water","drip","faucet","spring"]},"water-wave":{"a":"Water Wave","b":"1F30A","j":["ocean","water","wave","sea","nature","tsunami","disaster"]},"jackolantern":{"a":"Jack-O-Lantern","b":"1F383","j":["celebration","halloween","jack","jack-o-lantern","lantern","jack_o_lantern","light","pumpkin","creepy","fall"]},"christmas-tree":{"a":"Christmas Tree","b":"1F384","j":["celebration","Christmas","tree","festival","vacation","december","xmas"]},"fireworks":{"a":"Fireworks","b":"1F386","j":["celebration","photo","festival","carnival","congratulations"]},"sparkler":{"a":"Sparkler","b":"1F387","j":["celebration","fireworks","sparkle","stars","night","shine"]},"firecracker":{"a":"Firecracker","b":"1F9E8","j":["dynamite","explosive","fireworks","boom","explode","explosion"]},"sparkles":{"a":"Sparkles","b":"2728","j":["*","sparkle","star","stars","shine","shiny","cool","awesome","good","magic"]},"balloon":{"a":"Balloon","b":"1F388","j":["celebration","party","birthday","circus"]},"party-popper":{"a":"Party Popper","b":"1F389","j":["celebration","party","popper","tada","congratulations","birthday","magic","circus"]},"confetti-ball":{"a":"Confetti Ball","b":"1F38A","j":["ball","celebration","confetti","festival","party","birthday","circus"]},"tanabata-tree":{"a":"Tanabata Tree","b":"1F38B","j":["banner","celebration","Japanese","tree","plant","nature","branch","summer","bamboo","wish","star_festival","tanzaku"]},"pine-decoration":{"a":"Pine Decoration","b":"1F38D","j":["bamboo","celebration","Japanese","pine","japanese","plant","nature","vegetable","panda","new_years"]},"japanese-dolls":{"a":"Japanese Dolls","b":"1F38E","j":["celebration","doll","festival","Japanese","Japanese dolls","japanese","toy","kimono"]},"carp-streamer":{"a":"Carp Streamer","b":"1F38F","j":["carp","celebration","streamer","fish","japanese","koinobori","banner"]},"wind-chime":{"a":"Wind Chime","b":"1F390","j":["bell","celebration","chime","wind","nature","ding","spring"]},"moon-viewing-ceremony":{"a":"Moon Viewing Ceremony","b":"1F391","j":["celebration","ceremony","moon","photo","japan","asia","tsukimi"]},"red-envelope":{"a":"Red Envelope","b":"1F9E7","j":["gift","good luck","hóngbāo","lai see","money"]},"ribbon":{"a":"Ribbon","b":"1F380","j":["celebration","decoration","pink","girl","bowtie"]},"wrapped-gift":{"a":"Wrapped Gift","b":"1F381","j":["box","celebration","gift","present","wrapped","birthday","christmas","xmas"]},"reminder-ribbon":{"a":"Reminder Ribbon","b":"1F397-FE0F","j":["celebration","reminder","ribbon","sports","cause","support","awareness"]},"admission-tickets":{"a":"Admission Tickets","b":"1F39F-FE0F","j":["admission","ticket","sports","concert","entrance"]},"ticket":{"a":"Ticket","b":"1F3AB","j":["admission","event","concert","pass"]},"military-medal":{"a":"Military Medal","b":"1F396-FE0F","j":["celebration","medal","military","award","winning","army"]},"trophy":{"a":"Trophy","b":"1F3C6-FE0F","j":["prize","win","award","contest","place","ftw","ceremony"]},"sports-medal":{"a":"Sports Medal","b":"1F3C5","j":["medal","award","winning"]},"1st-place-medal":{"a":"1st Place Medal","b":"1F947","j":["first","gold","medal","award","winning"]},"2nd-place-medal":{"a":"2nd Place Medal","b":"1F948","j":["medal","second","silver","award"]},"3rd-place-medal":{"a":"3rd Place Medal","b":"1F949","j":["bronze","medal","third","award"]},"soccer-ball":{"a":"Soccer Ball","b":"26BD-FE0F","j":["ball","football","soccer","sports"]},"baseball":{"a":"Baseball","b":"26BE-FE0F","j":["ball","sports","balls"]},"softball":{"a":"Softball","b":"1F94E","j":["ball","glove","underarm","sports","balls"]},"basketball":{"a":"Basketball","b":"1F3C0","j":["ball","hoop","sports","balls","NBA"]},"volleyball":{"a":"Volleyball","b":"1F3D0","j":["ball","game","sports","balls"]},"american-football":{"a":"American Football","b":"1F3C8","j":["american","ball","football","sports","balls","NFL"]},"rugby-football":{"a":"Rugby Football","b":"1F3C9","j":["ball","football","rugby","sports","team"]},"tennis":{"a":"Tennis","b":"1F3BE","j":["ball","racquet","sports","balls","green"]},"flying-disc":{"a":"Flying Disc","b":"1F94F","j":["ultimate","sports","frisbee"]},"bowling":{"a":"Bowling","b":"1F3B3","j":["ball","game","sports","fun","play"]},"cricket-game":{"a":"Cricket Game","b":"1F3CF","j":["ball","bat","game","sports"]},"field-hockey":{"a":"Field Hockey","b":"1F3D1","j":["ball","field","game","hockey","stick","sports"]},"ice-hockey":{"a":"Ice Hockey","b":"1F3D2","j":["game","hockey","ice","puck","stick","sports"]},"lacrosse":{"a":"Lacrosse","b":"1F94D","j":["ball","goal","stick","sports"]},"ping-pong":{"a":"Ping Pong","b":"1F3D3","j":["ball","bat","game","paddle","table tennis","sports","pingpong"]},"badminton":{"a":"Badminton","b":"1F3F8","j":["birdie","game","racquet","shuttlecock","sports"]},"boxing-glove":{"a":"Boxing Glove","b":"1F94A","j":["boxing","glove","sports","fighting"]},"martial-arts-uniform":{"a":"Martial Arts Uniform","b":"1F94B","j":["judo","karate","martial arts","taekwondo","uniform"]},"goal-net":{"a":"Goal Net","b":"1F945","j":["goal","net","sports"]},"flag-in-hole":{"a":"Flag in Hole","b":"26F3-FE0F","j":["golf","hole","sports","business","flag","summer"]},"ice-skate":{"a":"Ice Skate","b":"26F8-FE0F","j":["ice","skate","sports"]},"fishing-pole":{"a":"Fishing Pole","b":"1F3A3","j":["fish","pole","food","hobby","summer"]},"diving-mask":{"a":"Diving Mask","b":"1F93F","j":["diving","scuba","snorkeling","sport","ocean"]},"running-shirt":{"a":"Running Shirt","b":"1F3BD","j":["athletics","running","sash","shirt","play","pageant"]},"skis":{"a":"Skis","b":"1F3BF","j":["ski","snow","sports","winter","cold"]},"sled":{"a":"Sled","b":"1F6F7","j":["sledge","sleigh","luge","toboggan"]},"curling-stone":{"a":"Curling Stone","b":"1F94C","j":["game","rock","sports"]},"bullseye":{"a":"Bullseye","b":"1F3AF","j":["dart","direct hit","game","hit","target","direct_hit","play","bar"]},"yoyo":{"a":"Yo-Yo","b":"1FA80","j":["fluctuate","toy","yo-yo","yo_yo"]},"kite":{"a":"Kite","b":"1FA81","j":["fly","soar","wind"]},"water-pistol":{"a":"Water Pistol","b":"1F52B","j":["gun","handgun","pistol","revolver","tool","water","weapon","violence"]},"pool-8-ball":{"a":"Pool 8 Ball","b":"1F3B1","j":["8","ball","billiard","eight","game","pool","hobby","luck","magic"]},"crystal-ball":{"a":"Crystal Ball","b":"1F52E","j":["ball","crystal","fairy tale","fantasy","fortune","tool","disco","party","magic","circus","fortune_teller"]},"magic-wand":{"a":"Magic Wand","b":"1FA84","j":["magic","witch","wizard","supernature","power"]},"video-game":{"a":"Video Game","b":"1F3AE-FE0F","j":["controller","game","play","console","PS4"]},"joystick":{"a":"Joystick","b":"1F579-FE0F","j":["game","video game","play"]},"slot-machine":{"a":"Slot Machine","b":"1F3B0","j":["game","slot","bet","gamble","vegas","fruit machine","luck","casino"]},"game-die":{"a":"Game Die","b":"1F3B2","j":["dice","die","game","random","tabletop","play","luck"]},"puzzle-piece":{"a":"Puzzle Piece","b":"1F9E9","j":["clue","interlocking","jigsaw","piece","puzzle"]},"teddy-bear":{"a":"Teddy Bear","b":"1F9F8","j":["plaything","plush","stuffed","toy"]},"piata":{"a":"Piñata","b":"1FA85","j":["celebration","party","piñata","pinata","mexico","candy"]},"mirror-ball":{"a":"Mirror Ball","b":"1FAA9","j":["dance","disco","glitter","party"]},"nesting-dolls":{"a":"Nesting Dolls","b":"1FA86","j":["doll","nesting","russia","matryoshka","toy"]},"spade-suit":{"a":"Spade Suit","b":"2660-FE0F","j":["card","game","poker","cards","suits","magic"]},"heart-suit":{"a":"Heart Suit","b":"2665-FE0F","j":["card","game","poker","cards","magic","suits"]},"diamond-suit":{"a":"Diamond Suit","b":"2666-FE0F","j":["card","game","poker","cards","magic","suits"]},"club-suit":{"a":"Club Suit","b":"2663-FE0F","j":["card","game","poker","cards","magic","suits"]},"chess-pawn":{"a":"Chess Pawn","b":"265F-FE0F","j":["chess","dupe","expendable"]},"joker":{"a":"Joker","b":"1F0CF","j":["card","game","wildcard","poker","cards","play","magic"]},"mahjong-red-dragon":{"a":"Mahjong Red Dragon","b":"1F004-FE0F","j":["game","mahjong","red","play","chinese","kanji"]},"flower-playing-cards":{"a":"Flower Playing Cards","b":"1F3B4","j":["card","flower","game","Japanese","playing","sunset","red"]},"performing-arts":{"a":"Performing Arts","b":"1F3AD-FE0F","j":["art","mask","performing","theater","theatre","acting","drama"]},"framed-picture":{"a":"Framed Picture","b":"1F5BC-FE0F","j":["art","frame","museum","painting","picture","photography"]},"artist-palette":{"a":"Artist Palette","b":"1F3A8","j":["art","museum","painting","palette","design","paint","draw","colors"]},"thread":{"a":"Thread","b":"1F9F5","j":["needle","sewing","spool","string"]},"sewing-needle":{"a":"Sewing Needle","b":"1FAA1","j":["embroidery","needle","sewing","stitches","sutures","tailoring"]},"yarn":{"a":"Yarn","b":"1F9F6","j":["ball","crochet","knit"]},"knot":{"a":"Knot","b":"1FAA2","j":["rope","tangled","tie","twine","twist","scout"]},"glasses":{"a":"Glasses","b":"1F453-FE0F","j":["clothing","eye","eyeglasses","eyewear","fashion","accessories","eyesight","nerdy","dork","geek"]},"sunglasses":{"a":"Sunglasses","b":"1F576-FE0F","j":["dark","eye","eyewear","glasses","face","cool","accessories"]},"goggles":{"a":"Goggles","b":"1F97D","j":["eye protection","swimming","welding","eyes","protection","safety"]},"lab-coat":{"a":"Lab Coat","b":"1F97C","j":["doctor","experiment","scientist","chemist"]},"safety-vest":{"a":"Safety Vest","b":"1F9BA","j":["emergency","safety","vest","protection"]},"necktie":{"a":"Necktie","b":"1F454","j":["clothing","tie","shirt","suitup","formal","fashion","cloth","business"]},"tshirt":{"a":"T-Shirt","b":"1F455","j":["clothing","shirt","t-shirt","t_shirt","fashion","cloth","casual","tee"]},"jeans":{"a":"Jeans","b":"1F456","j":["clothing","pants","trousers","fashion","shopping"]},"scarf":{"a":"Scarf","b":"1F9E3","j":["neck","winter","clothes"]},"gloves":{"a":"Gloves","b":"1F9E4","j":["hand","hands","winter","clothes"]},"coat":{"a":"Coat","b":"1F9E5","j":["jacket"]},"socks":{"a":"Socks","b":"1F9E6","j":["stocking","stockings","clothes"]},"dress":{"a":"Dress","b":"1F457","j":["clothing","clothes","fashion","shopping"]},"kimono":{"a":"Kimono","b":"1F458","j":["clothing","dress","fashion","women","female","japanese"]},"sari":{"a":"Sari","b":"1F97B","j":["clothing","dress"]},"onepiece-swimsuit":{"a":"One-Piece Swimsuit","b":"1FA71","j":["bathing suit","one-piece swimsuit","one_piece_swimsuit","fashion"]},"briefs":{"a":"Briefs","b":"1FA72","j":["bathing suit","one-piece","swimsuit","underwear","clothing"]},"shorts":{"a":"Shorts","b":"1FA73","j":["bathing suit","pants","underwear","clothing"]},"bikini":{"a":"Bikini","b":"1F459","j":["clothing","swim","swimming","female","woman","girl","fashion","beach","summer"]},"womans-clothes":{"a":"Woman’S Clothes","b":"1F45A","j":["clothing","woman","woman’s clothes","woman_s_clothes","fashion","shopping_bags","female"]},"folding-hand-fan":{"a":"⊛ Folding Hand Fan","b":"1FAAD","j":["cooling","dance","fan","flutter","hot","shy","flamenco"]},"purse":{"a":"Purse","b":"1F45B","j":["clothing","coin","fashion","accessories","money","sales","shopping"]},"handbag":{"a":"Handbag","b":"1F45C","j":["bag","clothing","purse","fashion","accessory","accessories","shopping"]},"clutch-bag":{"a":"Clutch Bag","b":"1F45D","j":["bag","clothing","pouch","accessories","shopping"]},"shopping-bags":{"a":"Shopping Bags","b":"1F6CD-FE0F","j":["bag","hotel","shopping","mall","buy","purchase"]},"backpack":{"a":"Backpack","b":"1F392","j":["bag","rucksack","satchel","school","student","education"]},"thong-sandal":{"a":"Thong Sandal","b":"1FA74","j":["beach sandals","sandals","thong sandals","thongs","zōri","footwear","summer"]},"mans-shoe":{"a":"Man’S Shoe","b":"1F45E","j":["clothing","man","man’s shoe","shoe","man_s_shoe","fashion","male"]},"running-shoe":{"a":"Running Shoe","b":"1F45F","j":["athletic","clothing","shoe","sneaker","shoes","sports","sneakers"]},"hiking-boot":{"a":"Hiking Boot","b":"1F97E","j":["backpacking","boot","camping","hiking"]},"flat-shoe":{"a":"Flat Shoe","b":"1F97F","j":["ballet flat","slip-on","slipper","ballet"]},"highheeled-shoe":{"a":"High-Heeled Shoe","b":"1F460","j":["clothing","heel","high-heeled shoe","shoe","woman","high_heeled_shoe","fashion","shoes","female","pumps","stiletto"]},"womans-sandal":{"a":"Woman’S Sandal","b":"1F461","j":["clothing","sandal","shoe","woman","woman’s sandal","woman_s_sandal","shoes","fashion","flip flops"]},"ballet-shoes":{"a":"Ballet Shoes","b":"1FA70","j":["ballet","dance"]},"womans-boot":{"a":"Woman’S Boot","b":"1F462","j":["boot","clothing","shoe","woman","woman’s boot","woman_s_boot","shoes","fashion"]},"hair-pick":{"a":"⊛ Hair Pick","b":"1FAAE","j":["Afro","comb","hair","pick","afro"]},"crown":{"a":"Crown","b":"1F451","j":["clothing","king","queen","kod","leader","royalty","lord"]},"womans-hat":{"a":"Woman’S Hat","b":"1F452","j":["clothing","hat","woman","woman’s hat","woman_s_hat","fashion","accessories","female","lady","spring"]},"top-hat":{"a":"Top Hat","b":"1F3A9","j":["clothing","hat","top","tophat","magic","gentleman","classy","circus"]},"graduation-cap":{"a":"Graduation Cap","b":"1F393-FE0F","j":["cap","celebration","clothing","graduation","hat","school","college","degree","university","legal","learn","education"]},"billed-cap":{"a":"Billed Cap","b":"1F9E2","j":["baseball cap","cap","baseball"]},"military-helmet":{"a":"Military Helmet","b":"1FA96","j":["army","helmet","military","soldier","warrior","protection"]},"rescue-workers-helmet":{"a":"Rescue Worker’S Helmet","b":"26D1-FE0F","j":["aid","cross","face","hat","helmet","rescue worker’s helmet","rescue_worker_s_helmet","construction","build"]},"prayer-beads":{"a":"Prayer Beads","b":"1F4FF","j":["beads","clothing","necklace","prayer","religion","dhikr","religious"]},"lipstick":{"a":"Lipstick","b":"1F484","j":["cosmetics","makeup","female","girl","fashion","woman"]},"ring":{"a":"Ring","b":"1F48D","j":["diamond","wedding","propose","marriage","valentines","fashion","jewelry","gem","engagement"]},"gem-stone":{"a":"Gem Stone","b":"1F48E","j":["diamond","gem","jewel","blue","ruby","jewelry"]},"muted-speaker":{"a":"Muted Speaker","b":"1F507","j":["mute","quiet","silent","speaker","sound","volume","silence"]},"speaker-low-volume":{"a":"Speaker Low Volume","b":"1F508-FE0F","j":["soft","sound","volume","silence","broadcast"]},"speaker-medium-volume":{"a":"Speaker Medium Volume","b":"1F509","j":["medium","volume","speaker","broadcast"]},"speaker-high-volume":{"a":"Speaker High Volume","b":"1F50A","j":["loud","volume","noise","noisy","speaker","broadcast"]},"loudspeaker":{"a":"Loudspeaker","b":"1F4E2","j":["loud","public address","volume","sound"]},"megaphone":{"a":"Megaphone","b":"1F4E3","j":["cheering","sound","speaker","volume"]},"postal-horn":{"a":"Postal Horn","b":"1F4EF","j":["horn","post","postal","instrument","music"]},"bell":{"a":"Bell","b":"1F514","j":["sound","notification","christmas","xmas","chime"]},"bell-with-slash":{"a":"Bell with Slash","b":"1F515","j":["bell","forbidden","mute","quiet","silent","sound","volume"]},"musical-score":{"a":"Musical Score","b":"1F3BC","j":["music","score","treble","clef","compose"]},"musical-note":{"a":"Musical Note","b":"1F3B5","j":["music","note","score","tone","sound"]},"musical-notes":{"a":"Musical Notes","b":"1F3B6","j":["music","note","notes","score"]},"studio-microphone":{"a":"Studio Microphone","b":"1F399-FE0F","j":["mic","microphone","music","studio","sing","recording","artist","talkshow"]},"level-slider":{"a":"Level Slider","b":"1F39A-FE0F","j":["level","music","slider","scale"]},"control-knobs":{"a":"Control Knobs","b":"1F39B-FE0F","j":["control","knobs","music","dial"]},"microphone":{"a":"Microphone","b":"1F3A4","j":["karaoke","mic","sound","music","PA","sing","talkshow"]},"headphone":{"a":"Headphone","b":"1F3A7-FE0F","j":["earbud","music","score","gadgets"]},"radio":{"a":"Radio","b":"1F4FB-FE0F","j":["video","communication","music","podcast","program"]},"saxophone":{"a":"Saxophone","b":"1F3B7","j":["instrument","music","sax","jazz","blues"]},"accordion":{"a":"Accordion","b":"1FA97","j":["concertina","squeeze box","music"]},"guitar":{"a":"Guitar","b":"1F3B8","j":["instrument","music"]},"musical-keyboard":{"a":"Musical Keyboard","b":"1F3B9","j":["instrument","keyboard","music","piano","compose"]},"trumpet":{"a":"Trumpet","b":"1F3BA","j":["instrument","music","brass"]},"violin":{"a":"Violin","b":"1F3BB","j":["instrument","music","orchestra","symphony"]},"banjo":{"a":"Banjo","b":"1FA95","j":["music","stringed","instructment"]},"drum":{"a":"Drum","b":"1F941","j":["drumsticks","music","instrument","snare"]},"long-drum":{"a":"Long Drum","b":"1FA98","j":["beat","conga","drum","rhythm","music"]},"maracas":{"a":"⊛ Maracas","b":"1FA87","j":["instrument","music","percussion","rattle","shake"]},"flute":{"a":"⊛ Flute","b":"1FA88","j":["fife","music","pipe","recorder","woodwind","bamboo","instrument","pied piper"]},"mobile-phone":{"a":"Mobile Phone","b":"1F4F1","j":["cell","mobile","phone","telephone","technology","apple","gadgets","dial"]},"mobile-phone-with-arrow":{"a":"Mobile Phone with Arrow","b":"1F4F2","j":["arrow","cell","mobile","phone","receive","iphone","incoming"]},"telephone":{"a":"Telephone","b":"260E-FE0F","j":["phone","technology","communication","dial"]},"telephone-receiver":{"a":"Telephone Receiver","b":"1F4DE","j":["phone","receiver","telephone","technology","communication","dial"]},"pager":{"a":"Pager","b":"1F4DF-FE0F","j":["bbcall","oldschool","90s"]},"fax-machine":{"a":"Fax Machine","b":"1F4E0","j":["fax","communication","technology"]},"battery":{"a":"Battery","b":"1F50B","j":["power","energy","sustain"]},"low-battery":{"a":"Low Battery","b":"1FAAB","j":["electronic","low energy","drained","dead"]},"electric-plug":{"a":"Electric Plug","b":"1F50C","j":["electric","electricity","plug","charger","power"]},"laptop":{"a":"Laptop","b":"1F4BB-FE0F","j":["computer","pc","personal","technology","screen","display","monitor"]},"desktop-computer":{"a":"Desktop Computer","b":"1F5A5-FE0F","j":["computer","desktop","technology","computing","screen"]},"printer":{"a":"Printer","b":"1F5A8-FE0F","j":["computer","paper","ink"]},"keyboard":{"a":"Keyboard","b":"2328-FE0F","j":["computer","technology","type","input","text"]},"computer-mouse":{"a":"Computer Mouse","b":"1F5B1-FE0F","j":["computer","click"]},"trackball":{"a":"Trackball","b":"1F5B2-FE0F","j":["computer","technology","trackpad"]},"computer-disk":{"a":"Computer Disk","b":"1F4BD","j":["computer","disk","minidisk","optical","technology","record","data","90s"]},"floppy-disk":{"a":"Floppy Disk","b":"1F4BE","j":["computer","disk","floppy","oldschool","technology","save","90s","80s"]},"optical-disk":{"a":"Optical Disk","b":"1F4BF-FE0F","j":["CD","computer","disk","optical","technology","dvd","disc","90s"]},"dvd":{"a":"Dvd","b":"1F4C0","j":["Blu-ray","computer","disk","DVD","optical","cd","disc"]},"abacus":{"a":"Abacus","b":"1F9EE","j":["calculation"]},"movie-camera":{"a":"Movie Camera","b":"1F3A5","j":["camera","cinema","movie","film","record"]},"film-frames":{"a":"Film Frames","b":"1F39E-FE0F","j":["cinema","film","frames","movie"]},"film-projector":{"a":"Film Projector","b":"1F4FD-FE0F","j":["cinema","film","movie","projector","video","tape","record"]},"clapper-board":{"a":"Clapper Board","b":"1F3AC-FE0F","j":["clapper","movie","film","record"]},"television":{"a":"Television","b":"1F4FA-FE0F","j":["tv","video","technology","program","oldschool","show"]},"camera":{"a":"Camera","b":"1F4F7-FE0F","j":["video","gadgets","photography"]},"camera-with-flash":{"a":"Camera with Flash","b":"1F4F8","j":["camera","flash","video","photography","gadgets"]},"video-camera":{"a":"Video Camera","b":"1F4F9-FE0F","j":["camera","video","film","record"]},"videocassette":{"a":"Videocassette","b":"1F4FC","j":["tape","vhs","video","record","oldschool","90s","80s"]},"magnifying-glass-tilted-left":{"a":"Magnifying Glass Tilted Left","b":"1F50D-FE0F","j":["glass","magnifying","search","tool","zoom","find","detective"]},"magnifying-glass-tilted-right":{"a":"Magnifying Glass Tilted Right","b":"1F50E","j":["glass","magnifying","search","tool","zoom","find","detective"]},"candle":{"a":"Candle","b":"1F56F-FE0F","j":["light","fire","wax"]},"light-bulb":{"a":"Light Bulb","b":"1F4A1","j":["bulb","comic","electric","idea","light","electricity"]},"flashlight":{"a":"Flashlight","b":"1F526","j":["electric","light","tool","torch","dark","camping","sight","night"]},"red-paper-lantern":{"a":"Red Paper Lantern","b":"1F3EE","j":["bar","lantern","light","red","paper","halloween","spooky"]},"diya-lamp":{"a":"Diya Lamp","b":"1FA94","j":["diya","lamp","oil","lighting"]},"notebook-with-decorative-cover":{"a":"Notebook with Decorative Cover","b":"1F4D4","j":["book","cover","decorated","notebook","classroom","notes","record","paper","study"]},"closed-book":{"a":"Closed Book","b":"1F4D5","j":["book","closed","read","library","knowledge","textbook","learn"]},"open-book":{"a":"Open Book","b":"1F4D6","j":["book","open","read","library","knowledge","literature","learn","study"]},"green-book":{"a":"Green Book","b":"1F4D7","j":["book","green","read","library","knowledge","study"]},"blue-book":{"a":"Blue Book","b":"1F4D8","j":["blue","book","read","library","knowledge","learn","study"]},"orange-book":{"a":"Orange Book","b":"1F4D9","j":["book","orange","read","library","knowledge","textbook","study"]},"books":{"a":"Books","b":"1F4DA-FE0F","j":["book","literature","library","study"]},"notebook":{"a":"Notebook","b":"1F4D3","j":["stationery","record","notes","paper","study"]},"ledger":{"a":"Ledger","b":"1F4D2","j":["notebook","notes","paper"]},"page-with-curl":{"a":"Page with Curl","b":"1F4C3","j":["curl","document","page","documents","office","paper"]},"scroll":{"a":"Scroll","b":"1F4DC","j":["paper","documents","ancient","history"]},"page-facing-up":{"a":"Page Facing Up","b":"1F4C4","j":["document","page","documents","office","paper","information"]},"newspaper":{"a":"Newspaper","b":"1F4F0","j":["news","paper","press","headline"]},"rolledup-newspaper":{"a":"Rolled-Up Newspaper","b":"1F5DE-FE0F","j":["news","newspaper","paper","rolled","rolled-up newspaper","rolled_up_newspaper","press","headline"]},"bookmark-tabs":{"a":"Bookmark Tabs","b":"1F4D1","j":["bookmark","mark","marker","tabs","favorite","save","order","tidy"]},"bookmark":{"a":"Bookmark","b":"1F516","j":["mark","favorite","label","save"]},"label":{"a":"Label","b":"1F3F7-FE0F","j":["sale","tag"]},"money-bag":{"a":"Money Bag","b":"1F4B0-FE0F","j":["bag","dollar","money","moneybag","payment","coins","sale"]},"coin":{"a":"Coin","b":"1FA99","j":["gold","metal","money","silver","treasure","currency"]},"yen-banknote":{"a":"Yen Banknote","b":"1F4B4","j":["banknote","bill","currency","money","note","yen","sales","japanese","dollar"]},"dollar-banknote":{"a":"Dollar Banknote","b":"1F4B5","j":["banknote","bill","currency","dollar","money","note","sales"]},"euro-banknote":{"a":"Euro Banknote","b":"1F4B6","j":["banknote","bill","currency","euro","money","note","sales","dollar"]},"pound-banknote":{"a":"Pound Banknote","b":"1F4B7","j":["banknote","bill","currency","money","note","pound","british","sterling","sales","bills","uk","england"]},"money-with-wings":{"a":"Money with Wings","b":"1F4B8","j":["banknote","bill","fly","money","wings","dollar","bills","payment","sale"]},"credit-card":{"a":"Credit Card","b":"1F4B3-FE0F","j":["card","credit","money","sales","dollar","bill","payment","shopping"]},"receipt":{"a":"Receipt","b":"1F9FE","j":["accounting","bookkeeping","evidence","proof","expenses"]},"chart-increasing-with-yen":{"a":"Chart Increasing with Yen","b":"1F4B9","j":["chart","graph","growth","money","yen","green-square","presentation","stats"]},"envelope":{"a":"Envelope","b":"2709-FE0F","j":["email","letter","postal","inbox","communication"]},"email":{"a":"E-Mail","b":"1F4E7","j":["e-mail","letter","mail","e_mail","communication","inbox"]},"incoming-envelope":{"a":"Incoming Envelope","b":"1F4E8","j":["e-mail","email","envelope","incoming","letter","receive","inbox"]},"envelope-with-arrow":{"a":"Envelope with Arrow","b":"1F4E9","j":["arrow","e-mail","email","envelope","outgoing","communication"]},"outbox-tray":{"a":"Outbox Tray","b":"1F4E4-FE0F","j":["box","letter","mail","outbox","sent","tray","inbox","email"]},"inbox-tray":{"a":"Inbox Tray","b":"1F4E5-FE0F","j":["box","inbox","letter","mail","receive","tray","email","documents"]},"package":{"a":"Package","b":"1F4E6-FE0F","j":["box","parcel","mail","gift","cardboard","moving"]},"closed-mailbox-with-raised-flag":{"a":"Closed Mailbox with Raised Flag","b":"1F4EB-FE0F","j":["closed","mail","mailbox","postbox","email","inbox","communication"]},"closed-mailbox-with-lowered-flag":{"a":"Closed Mailbox with Lowered Flag","b":"1F4EA-FE0F","j":["closed","lowered","mail","mailbox","postbox","email","communication","inbox"]},"open-mailbox-with-raised-flag":{"a":"Open Mailbox with Raised Flag","b":"1F4EC-FE0F","j":["mail","mailbox","open","postbox","email","inbox","communication"]},"open-mailbox-with-lowered-flag":{"a":"Open Mailbox with Lowered Flag","b":"1F4ED-FE0F","j":["lowered","mail","mailbox","open","postbox","email","inbox"]},"postbox":{"a":"Postbox","b":"1F4EE","j":["mail","mailbox","email","letter","envelope"]},"ballot-box-with-ballot":{"a":"Ballot Box with Ballot","b":"1F5F3-FE0F","j":["ballot","box","election","vote"]},"pencil":{"a":"Pencil","b":"270F-FE0F","j":["stationery","write","paper","writing","school","study"]},"black-nib":{"a":"Black Nib","b":"2712-FE0F","j":["nib","pen","stationery","writing","write"]},"fountain-pen":{"a":"Fountain Pen","b":"1F58B-FE0F","j":["fountain","pen","stationery","writing","write"]},"pen":{"a":"Pen","b":"1F58A-FE0F","j":["ballpoint","stationery","writing","write"]},"paintbrush":{"a":"Paintbrush","b":"1F58C-FE0F","j":["painting","drawing","creativity","art"]},"crayon":{"a":"Crayon","b":"1F58D-FE0F","j":["drawing","creativity"]},"memo":{"a":"Memo","b":"1F4DD","j":["pencil","write","documents","stationery","paper","writing","legal","exam","quiz","test","study","compose"]},"briefcase":{"a":"Briefcase","b":"1F4BC","j":["business","documents","work","law","legal","job","career"]},"file-folder":{"a":"File Folder","b":"1F4C1","j":["file","folder","documents","business","office"]},"open-file-folder":{"a":"Open File Folder","b":"1F4C2","j":["file","folder","open","documents","load"]},"card-index-dividers":{"a":"Card Index Dividers","b":"1F5C2-FE0F","j":["card","dividers","index","organizing","business","stationery"]},"calendar":{"a":"Calendar","b":"1F4C5","j":["date","schedule"]},"tearoff-calendar":{"a":"Tear-off Calendar","b":"1F4C6","j":["calendar","tear-off calendar","tear_off_calendar","schedule","date","planning"]},"spiral-notepad":{"a":"Spiral Notepad","b":"1F5D2-FE0F","j":["note","pad","spiral","memo","stationery"]},"spiral-calendar":{"a":"Spiral Calendar","b":"1F5D3-FE0F","j":["calendar","pad","spiral","date","schedule","planning"]},"card-index":{"a":"Card Index","b":"1F4C7","j":["card","index","rolodex","business","stationery"]},"chart-increasing":{"a":"Chart Increasing","b":"1F4C8","j":["chart","graph","growth","trend","upward","presentation","stats","recovery","business","economics","money","sales","good","success"]},"chart-decreasing":{"a":"Chart Decreasing","b":"1F4C9","j":["chart","down","graph","trend","presentation","stats","recession","business","economics","money","sales","bad","failure"]},"bar-chart":{"a":"Bar Chart","b":"1F4CA","j":["bar","chart","graph","presentation","stats"]},"clipboard":{"a":"Clipboard","b":"1F4CB-FE0F","j":["stationery","documents"]},"pushpin":{"a":"Pushpin","b":"1F4CC","j":["pin","stationery","mark","here"]},"round-pushpin":{"a":"Round Pushpin","b":"1F4CD","j":["pin","pushpin","stationery","location","map","here"]},"paperclip":{"a":"Paperclip","b":"1F4CE","j":["documents","stationery"]},"linked-paperclips":{"a":"Linked Paperclips","b":"1F587-FE0F","j":["link","paperclip","documents","stationery"]},"straight-ruler":{"a":"Straight Ruler","b":"1F4CF","j":["ruler","straight edge","stationery","calculate","length","math","school","drawing","architect","sketch"]},"triangular-ruler":{"a":"Triangular Ruler","b":"1F4D0","j":["ruler","set","triangle","stationery","math","architect","sketch"]},"scissors":{"a":"Scissors","b":"2702-FE0F","j":["cutting","tool","stationery","cut"]},"card-file-box":{"a":"Card File Box","b":"1F5C3-FE0F","j":["box","card","file","business","stationery"]},"file-cabinet":{"a":"File Cabinet","b":"1F5C4-FE0F","j":["cabinet","file","filing","organizing"]},"wastebasket":{"a":"Wastebasket","b":"1F5D1-FE0F","j":["bin","trash","rubbish","garbage","toss"]},"locked":{"a":"Locked","b":"1F512-FE0F","j":["closed","security","password","padlock"]},"unlocked":{"a":"Unlocked","b":"1F513-FE0F","j":["lock","open","unlock","privacy","security"]},"locked-with-pen":{"a":"Locked with Pen","b":"1F50F","j":["ink","lock","nib","pen","privacy","security","secret"]},"locked-with-key":{"a":"Locked with Key","b":"1F510","j":["closed","key","lock","secure","security","privacy"]},"key":{"a":"Key","b":"1F511","j":["lock","password","door"]},"old-key":{"a":"Old Key","b":"1F5DD-FE0F","j":["clue","key","lock","old","door","password"]},"hammer":{"a":"Hammer","b":"1F528","j":["tool","tools","build","create"]},"axe":{"a":"Axe","b":"1FA93","j":["chop","hatchet","split","wood","tool","cut"]},"pick":{"a":"Pick","b":"26CF-FE0F","j":["mining","tool","tools","dig"]},"hammer-and-pick":{"a":"Hammer and Pick","b":"2692-FE0F","j":["hammer","pick","tool","tools","build","create"]},"hammer-and-wrench":{"a":"Hammer and Wrench","b":"1F6E0-FE0F","j":["hammer","spanner","tool","wrench","tools","build","create"]},"dagger":{"a":"Dagger","b":"1F5E1-FE0F","j":["knife","weapon"]},"crossed-swords":{"a":"Crossed Swords","b":"2694-FE0F","j":["crossed","swords","weapon"]},"bomb":{"a":"Bomb","b":"1F4A3-FE0F","j":["comic","boom","explode","explosion","terrorism"]},"boomerang":{"a":"Boomerang","b":"1FA83","j":["australia","rebound","repercussion","weapon"]},"bow-and-arrow":{"a":"Bow and Arrow","b":"1F3F9","j":["archer","arrow","bow","Sagittarius","zodiac","sports"]},"shield":{"a":"Shield","b":"1F6E1-FE0F","j":["weapon","protection","security"]},"carpentry-saw":{"a":"Carpentry Saw","b":"1FA9A","j":["carpenter","lumber","saw","tool","cut","chop"]},"wrench":{"a":"Wrench","b":"1F527","j":["spanner","tool","tools","diy","ikea","fix","maintainer"]},"screwdriver":{"a":"Screwdriver","b":"1FA9B","j":["screw","tool","tools"]},"nut-and-bolt":{"a":"Nut and Bolt","b":"1F529","j":["bolt","nut","tool","handy","tools","fix"]},"gear":{"a":"Gear","b":"2699-FE0F","j":["cog","cogwheel","tool"]},"clamp":{"a":"Clamp","b":"1F5DC-FE0F","j":["compress","tool","vice"]},"balance-scale":{"a":"Balance Scale","b":"2696-FE0F","j":["balance","justice","Libra","scale","zodiac","law","fairness","weight"]},"white-cane":{"a":"White Cane","b":"1F9AF","j":["accessibility","blind","probing_cane"]},"link":{"a":"Link","b":"1F517","j":["rings","url"]},"chains":{"a":"Chains","b":"26D3-FE0F","j":["chain","lock","arrest"]},"hook":{"a":"Hook","b":"1FA9D","j":["catch","crook","curve","ensnare","selling point","tools"]},"toolbox":{"a":"Toolbox","b":"1F9F0","j":["chest","mechanic","tool","tools","diy","fix","maintainer"]},"magnet":{"a":"Magnet","b":"1F9F2","j":["attraction","horseshoe","magnetic"]},"ladder":{"a":"Ladder","b":"1FA9C","j":["climb","rung","step","tools"]},"alembic":{"a":"Alembic","b":"2697-FE0F","j":["chemistry","tool","distilling","science","experiment"]},"test-tube":{"a":"Test Tube","b":"1F9EA","j":["chemist","chemistry","experiment","lab","science"]},"petri-dish":{"a":"Petri Dish","b":"1F9EB","j":["bacteria","biologist","biology","culture","lab"]},"dna":{"a":"Dna","b":"1F9EC","j":["biologist","evolution","gene","genetics","life"]},"microscope":{"a":"Microscope","b":"1F52C","j":["science","tool","laboratory","experiment","zoomin","study"]},"telescope":{"a":"Telescope","b":"1F52D","j":["science","tool","stars","space","zoom","astronomy"]},"satellite-antenna":{"a":"Satellite Antenna","b":"1F4E1","j":["antenna","dish","satellite","communication","future","radio","space"]},"syringe":{"a":"Syringe","b":"1F489","j":["medicine","needle","shot","sick","health","hospital","drugs","blood","doctor","nurse"]},"drop-of-blood":{"a":"Drop of Blood","b":"1FA78","j":["bleed","blood donation","injury","medicine","menstruation","period","hurt","harm","wound"]},"pill":{"a":"Pill","b":"1F48A","j":["doctor","medicine","sick","health","pharmacy","drug"]},"adhesive-bandage":{"a":"Adhesive Bandage","b":"1FA79","j":["bandage","heal"]},"crutch":{"a":"Crutch","b":"1FA7C","j":["cane","disability","hurt","mobility aid","stick","accessibility","assist"]},"stethoscope":{"a":"Stethoscope","b":"1FA7A","j":["doctor","heart","medicine","health"]},"xray":{"a":"X-Ray","b":"1FA7B","j":["bones","doctor","medical","skeleton","x-ray","medicine"]},"door":{"a":"Door","b":"1F6AA","j":["house","entry","exit"]},"elevator":{"a":"Elevator","b":"1F6D7","j":["accessibility","hoist","lift"]},"mirror":{"a":"Mirror","b":"1FA9E","j":["reflection","reflector","speculum"]},"window":{"a":"Window","b":"1FA9F","j":["frame","fresh air","opening","transparent","view","scenery"]},"bed":{"a":"Bed","b":"1F6CF-FE0F","j":["hotel","sleep","rest"]},"couch-and-lamp":{"a":"Couch and Lamp","b":"1F6CB-FE0F","j":["couch","hotel","lamp","read","chill"]},"chair":{"a":"Chair","b":"1FA91","j":["seat","sit","furniture"]},"toilet":{"a":"Toilet","b":"1F6BD","j":["restroom","wc","washroom","bathroom","potty"]},"plunger":{"a":"Plunger","b":"1FAA0","j":["force cup","plumber","suction","toilet"]},"shower":{"a":"Shower","b":"1F6BF","j":["water","clean","bathroom"]},"bathtub":{"a":"Bathtub","b":"1F6C1","j":["bath","clean","shower","bathroom"]},"mouse-trap":{"a":"Mouse Trap","b":"1FAA4","j":["bait","mousetrap","snare","trap","cheese"]},"razor":{"a":"Razor","b":"1FA92","j":["sharp","shave","cut"]},"lotion-bottle":{"a":"Lotion Bottle","b":"1F9F4","j":["lotion","moisturizer","shampoo","sunscreen"]},"safety-pin":{"a":"Safety Pin","b":"1F9F7","j":["diaper","punk rock"]},"broom":{"a":"Broom","b":"1F9F9","j":["cleaning","sweeping","witch"]},"basket":{"a":"Basket","b":"1F9FA","j":["farming","laundry","picnic"]},"roll-of-paper":{"a":"Roll of Paper","b":"1F9FB","j":["paper towels","toilet paper","roll"]},"bucket":{"a":"Bucket","b":"1FAA3","j":["cask","pail","vat","water","container"]},"soap":{"a":"Soap","b":"1F9FC","j":["bar","bathing","cleaning","lather","soapdish"]},"bubbles":{"a":"Bubbles","b":"1FAE7","j":["burp","clean","soap","underwater","fun","carbonation","sparkling"]},"toothbrush":{"a":"Toothbrush","b":"1FAA5","j":["bathroom","brush","clean","dental","hygiene","teeth"]},"sponge":{"a":"Sponge","b":"1F9FD","j":["absorbing","cleaning","porous"]},"fire-extinguisher":{"a":"Fire Extinguisher","b":"1F9EF","j":["extinguish","fire","quench"]},"shopping-cart":{"a":"Shopping Cart","b":"1F6D2","j":["cart","shopping","trolley"]},"cigarette":{"a":"Cigarette","b":"1F6AC","j":["smoking","kills","tobacco","joint","smoke"]},"coffin":{"a":"Coffin","b":"26B0-FE0F","j":["death","vampire","dead","die","rip","graveyard","cemetery","casket","funeral","box"]},"headstone":{"a":"Headstone","b":"1FAA6","j":["cemetery","grave","graveyard","tombstone","death","rip"]},"funeral-urn":{"a":"Funeral Urn","b":"26B1-FE0F","j":["ashes","death","funeral","urn","dead","die","rip"]},"nazar-amulet":{"a":"Nazar Amulet","b":"1F9FF","j":["bead","charm","evil-eye","nazar","talisman"]},"hamsa":{"a":"Hamsa","b":"1FAAC","j":["amulet","Fatima","hand","Mary","Miriam","protection","religion"]},"moai":{"a":"Moai","b":"1F5FF","j":["face","moyai","statue","rock","easter island"]},"placard":{"a":"Placard","b":"1FAA7","j":["demonstration","picket","protest","sign","announcement"]},"identification-card":{"a":"Identification Card","b":"1FAAA","j":["credentials","ID","license","security","document"]},"atm-sign":{"a":"Atm Sign","b":"1F3E7","j":["ATM","ATM sign","automated","bank","teller","money","sales","cash","blue-square","payment"]},"litter-in-bin-sign":{"a":"Litter in Bin Sign","b":"1F6AE","j":["litter","litter bin","blue-square","sign","human","info"]},"potable-water":{"a":"Potable Water","b":"1F6B0","j":["drinking","potable","water","blue-square","liquid","restroom","cleaning","faucet"]},"wheelchair-symbol":{"a":"Wheelchair Symbol","b":"267F-FE0F","j":["access","blue-square","disabled","accessibility"]},"mens-room":{"a":"Men’S Room","b":"1F6B9-FE0F","j":["bathroom","lavatory","man","men’s room","restroom","toilet","WC","men_s_room","wc","blue-square","gender","male"]},"womens-room":{"a":"Women’S Room","b":"1F6BA-FE0F","j":["bathroom","lavatory","restroom","toilet","WC","woman","women’s room","women_s_room","purple-square","female","loo","gender"]},"restroom":{"a":"Restroom","b":"1F6BB","j":["bathroom","lavatory","toilet","WC","blue-square","refresh","wc","gender"]},"baby-symbol":{"a":"Baby Symbol","b":"1F6BC-FE0F","j":["baby","changing","orange-square","child"]},"water-closet":{"a":"Water Closet","b":"1F6BE","j":["bathroom","closet","lavatory","restroom","toilet","water","WC","blue-square"]},"passport-control":{"a":"Passport Control","b":"1F6C2","j":["control","passport","custom","blue-square"]},"customs":{"a":"Customs","b":"1F6C3","j":["passport","border","blue-square"]},"baggage-claim":{"a":"Baggage Claim","b":"1F6C4","j":["baggage","claim","blue-square","airport","transport"]},"left-luggage":{"a":"Left Luggage","b":"1F6C5","j":["baggage","locker","luggage","blue-square","travel"]},"warning":{"a":"Warning","b":"26A0-FE0F","j":["exclamation","wip","alert","error","problem","issue"]},"children-crossing":{"a":"Children Crossing","b":"1F6B8","j":["child","crossing","pedestrian","traffic","school","warning","danger","sign","driving","yellow-diamond"]},"no-entry":{"a":"No Entry","b":"26D4-FE0F","j":["entry","forbidden","no","not","prohibited","traffic","limit","security","privacy","bad","denied","stop","circle"]},"prohibited":{"a":"Prohibited","b":"1F6AB","j":["entry","forbidden","no","not","forbid","stop","limit","denied","disallow","circle"]},"no-bicycles":{"a":"No Bicycles","b":"1F6B3","j":["bicycle","bike","forbidden","no","prohibited","no_bikes","cyclist","circle"]},"no-smoking":{"a":"No Smoking","b":"1F6AD-FE0F","j":["forbidden","no","not","prohibited","smoking","cigarette","blue-square","smell","smoke"]},"no-littering":{"a":"No Littering","b":"1F6AF","j":["forbidden","litter","no","not","prohibited","trash","bin","garbage","circle"]},"nonpotable-water":{"a":"Non-Potable Water","b":"1F6B1","j":["non-drinking","non-potable","water","non_potable_water","drink","faucet","tap","circle"]},"no-pedestrians":{"a":"No Pedestrians","b":"1F6B7","j":["forbidden","no","not","pedestrian","prohibited","rules","crossing","walking","circle"]},"no-mobile-phones":{"a":"No Mobile Phones","b":"1F4F5","j":["cell","forbidden","mobile","no","phone","iphone","mute","circle"]},"no-one-under-eighteen":{"a":"No One Under Eighteen","b":"1F51E","j":["18","age restriction","eighteen","prohibited","underage","drink","pub","night","minor","circle"]},"radioactive":{"a":"Radioactive","b":"2622-FE0F","j":["sign","nuclear","danger"]},"biohazard":{"a":"Biohazard","b":"2623-FE0F","j":["sign","danger"]},"up-arrow":{"a":"Up Arrow","b":"2B06-FE0F","j":["arrow","cardinal","direction","north","blue-square","continue","top"]},"upright-arrow":{"a":"Up-Right Arrow","b":"2197-FE0F","j":["arrow","direction","intercardinal","northeast","up-right arrow","up_right_arrow","blue-square","point","diagonal"]},"right-arrow":{"a":"Right Arrow","b":"27A1-FE0F","j":["arrow","cardinal","direction","east","blue-square","next"]},"downright-arrow":{"a":"Down-Right Arrow","b":"2198-FE0F","j":["arrow","direction","down-right arrow","intercardinal","southeast","down_right_arrow","blue-square","diagonal"]},"down-arrow":{"a":"Down Arrow","b":"2B07-FE0F","j":["arrow","cardinal","direction","down","south","blue-square","bottom"]},"downleft-arrow":{"a":"Down-Left Arrow","b":"2199-FE0F","j":["arrow","direction","down-left arrow","intercardinal","southwest","down_left_arrow","blue-square","diagonal"]},"left-arrow":{"a":"Left Arrow","b":"2B05-FE0F","j":["arrow","cardinal","direction","west","blue-square","previous","back"]},"upleft-arrow":{"a":"Up-Left Arrow","b":"2196-FE0F","j":["arrow","direction","intercardinal","northwest","up-left arrow","up_left_arrow","blue-square","point","diagonal"]},"updown-arrow":{"a":"Up-Down Arrow","b":"2195-FE0F","j":["arrow","up-down arrow","up_down_arrow","blue-square","direction","way","vertical"]},"leftright-arrow":{"a":"Left-Right Arrow","b":"2194-FE0F","j":["arrow","left-right arrow","left_right_arrow","shape","direction","horizontal","sideways"]},"right-arrow-curving-left":{"a":"Right Arrow Curving Left","b":"21A9-FE0F","j":["arrow","back","return","blue-square","undo","enter"]},"left-arrow-curving-right":{"a":"Left Arrow Curving Right","b":"21AA-FE0F","j":["arrow","blue-square","return","rotate","direction"]},"right-arrow-curving-up":{"a":"Right Arrow Curving Up","b":"2934-FE0F","j":["arrow","blue-square","direction","top"]},"right-arrow-curving-down":{"a":"Right Arrow Curving Down","b":"2935-FE0F","j":["arrow","down","blue-square","direction","bottom"]},"clockwise-vertical-arrows":{"a":"Clockwise Vertical Arrows","b":"1F503","j":["arrow","clockwise","reload","sync","cycle","round","repeat"]},"counterclockwise-arrows-button":{"a":"Counterclockwise Arrows Button","b":"1F504","j":["anticlockwise","arrow","counterclockwise","withershins","blue-square","sync","cycle"]},"back-arrow":{"a":"Back Arrow","b":"1F519","j":["arrow","BACK","words","return"]},"end-arrow":{"a":"End Arrow","b":"1F51A","j":["arrow","END","words"]},"on-arrow":{"a":"On! Arrow","b":"1F51B","j":["arrow","mark","ON","ON!","words"]},"soon-arrow":{"a":"Soon Arrow","b":"1F51C","j":["arrow","SOON","words"]},"top-arrow":{"a":"Top Arrow","b":"1F51D","j":["arrow","TOP","up","words","blue-square"]},"place-of-worship":{"a":"Place of Worship","b":"1F6D0","j":["religion","worship","church","temple","prayer"]},"atom-symbol":{"a":"Atom Symbol","b":"269B-FE0F","j":["atheist","atom","science","physics","chemistry"]},"om":{"a":"Om","b":"1F549-FE0F","j":["Hindu","religion","hinduism","buddhism","sikhism","jainism"]},"star-of-david":{"a":"Star of David","b":"2721-FE0F","j":["David","Jew","Jewish","religion","star","star of David","judaism"]},"wheel-of-dharma":{"a":"Wheel of Dharma","b":"2638-FE0F","j":["Buddhist","dharma","religion","wheel","hinduism","buddhism","sikhism","jainism"]},"yin-yang":{"a":"Yin Yang","b":"262F-FE0F","j":["religion","tao","taoist","yang","yin","balance"]},"latin-cross":{"a":"Latin Cross","b":"271D-FE0F","j":["Christian","cross","religion","christianity"]},"orthodox-cross":{"a":"Orthodox Cross","b":"2626-FE0F","j":["Christian","cross","religion","suppedaneum"]},"star-and-crescent":{"a":"Star and Crescent","b":"262A-FE0F","j":["islam","Muslim","religion"]},"peace-symbol":{"a":"Peace Symbol","b":"262E-FE0F","j":["peace","hippie"]},"menorah":{"a":"Menorah","b":"1F54E","j":["candelabrum","candlestick","religion","hanukkah","candles","jewish"]},"dotted-sixpointed-star":{"a":"Dotted Six-Pointed Star","b":"1F52F","j":["dotted six-pointed star","fortune","star","dotted_six_pointed_star","purple-square","religion","jewish","hexagram"]},"khanda":{"a":"⊛ Khanda","b":"1FAAF","j":["religion","Sikh","Sikhism"]},"aries":{"a":"Aries","b":"2648-FE0F","j":["ram","zodiac","sign","purple-square","astrology"]},"taurus":{"a":"Taurus","b":"2649-FE0F","j":["bull","ox","zodiac","purple-square","sign","astrology"]},"gemini":{"a":"Gemini","b":"264A-FE0F","j":["twins","zodiac","sign","purple-square","astrology"]},"cancer":{"a":"Cancer","b":"264B-FE0F","j":["crab","zodiac","sign","purple-square","astrology"]},"leo":{"a":"Leo","b":"264C-FE0F","j":["lion","zodiac","sign","purple-square","astrology"]},"virgo":{"a":"Virgo","b":"264D-FE0F","j":["zodiac","sign","purple-square","astrology"]},"libra":{"a":"Libra","b":"264E-FE0F","j":["balance","justice","scales","zodiac","sign","purple-square","astrology"]},"scorpio":{"a":"Scorpio","b":"264F-FE0F","j":["scorpion","scorpius","zodiac","sign","purple-square","astrology"]},"sagittarius":{"a":"Sagittarius","b":"2650-FE0F","j":["archer","zodiac","sign","purple-square","astrology"]},"capricorn":{"a":"Capricorn","b":"2651-FE0F","j":["goat","zodiac","sign","purple-square","astrology"]},"aquarius":{"a":"Aquarius","b":"2652-FE0F","j":["bearer","water","zodiac","sign","purple-square","astrology"]},"pisces":{"a":"Pisces","b":"2653-FE0F","j":["fish","zodiac","purple-square","sign","astrology"]},"ophiuchus":{"a":"Ophiuchus","b":"26CE","j":["bearer","serpent","snake","zodiac","sign","purple-square","constellation","astrology"]},"shuffle-tracks-button":{"a":"Shuffle Tracks Button","b":"1F500","j":["arrow","crossed","blue-square","shuffle","music","random"]},"repeat-button":{"a":"Repeat Button","b":"1F501","j":["arrow","clockwise","repeat","loop","record"]},"repeat-single-button":{"a":"Repeat Single Button","b":"1F502","j":["arrow","clockwise","once","blue-square","loop"]},"play-button":{"a":"Play Button","b":"25B6-FE0F","j":["arrow","play","right","triangle","blue-square","direction"]},"fastforward-button":{"a":"Fast-Forward Button","b":"23E9-FE0F","j":["arrow","double","fast","fast-forward button","forward","fast_forward_button","blue-square","play","speed","continue"]},"next-track-button":{"a":"Next Track Button","b":"23ED-FE0F","j":["arrow","next scene","next track","triangle","forward","next","blue-square"]},"play-or-pause-button":{"a":"Play or Pause Button","b":"23EF-FE0F","j":["arrow","pause","play","right","triangle","blue-square"]},"reverse-button":{"a":"Reverse Button","b":"25C0-FE0F","j":["arrow","left","reverse","triangle","blue-square","direction"]},"fast-reverse-button":{"a":"Fast Reverse Button","b":"23EA-FE0F","j":["arrow","double","rewind","play","blue-square"]},"last-track-button":{"a":"Last Track Button","b":"23EE-FE0F","j":["arrow","previous scene","previous track","triangle","backward"]},"upwards-button":{"a":"Upwards Button","b":"1F53C","j":["arrow","button","blue-square","triangle","direction","point","forward","top"]},"fast-up-button":{"a":"Fast Up Button","b":"23EB","j":["arrow","double","blue-square","direction","top"]},"downwards-button":{"a":"Downwards Button","b":"1F53D","j":["arrow","button","down","blue-square","direction","bottom"]},"fast-down-button":{"a":"Fast Down Button","b":"23EC","j":["arrow","double","down","blue-square","direction","bottom"]},"pause-button":{"a":"Pause Button","b":"23F8-FE0F","j":["bar","double","pause","vertical","blue-square"]},"stop-button":{"a":"Stop Button","b":"23F9-FE0F","j":["square","stop","blue-square"]},"record-button":{"a":"Record Button","b":"23FA-FE0F","j":["circle","record","blue-square"]},"eject-button":{"a":"Eject Button","b":"23CF-FE0F","j":["eject","blue-square"]},"cinema":{"a":"Cinema","b":"1F3A6","j":["camera","film","movie","blue-square","record","curtain","stage","theater"]},"dim-button":{"a":"Dim Button","b":"1F505","j":["brightness","dim","low","sun","afternoon","warm","summer"]},"bright-button":{"a":"Bright Button","b":"1F506","j":["bright","brightness","sun","light"]},"antenna-bars":{"a":"Antenna Bars","b":"1F4F6","j":["antenna","bar","cell","mobile","phone","blue-square","reception","internet","connection","wifi","bluetooth","bars"]},"wireless":{"a":"⊛ Wireless","b":"1F6DC","j":["computer","internet","network","wifi","contactless","signal"]},"vibration-mode":{"a":"Vibration Mode","b":"1F4F3","j":["cell","mobile","mode","phone","telephone","vibration","orange-square"]},"mobile-phone-off":{"a":"Mobile Phone off","b":"1F4F4","j":["cell","mobile","off","phone","telephone","mute","orange-square","silence","quiet"]},"female-sign":{"a":"Female Sign","b":"2640-FE0F","j":["woman","women","lady","girl"]},"male-sign":{"a":"Male Sign","b":"2642-FE0F","j":["man","boy","men"]},"transgender-symbol":{"a":"Transgender Symbol","b":"26A7-FE0F","j":["transgender","lgbtq"]},"multiply":{"a":"Multiply","b":"2716-FE0F","j":["×","cancel","multiplication","sign","x","multiplication_sign","math","calculation"]},"plus":{"a":"Plus","b":"2795","j":["+","math","sign","plus_sign","calculation","addition","more","increase"]},"minus":{"a":"Minus","b":"2796","j":["-","−","math","sign","minus_sign","calculation","subtract","less"]},"divide":{"a":"Divide","b":"2797","j":["÷","division","math","sign","division_sign","calculation"]},"heavy-equals-sign":{"a":"Heavy Equals Sign","b":"1F7F0","j":["equality","math"]},"infinity":{"a":"Infinity","b":"267E-FE0F","j":["forever","unbounded","universal"]},"double-exclamation-mark":{"a":"Double Exclamation Mark","b":"203C-FE0F","j":["!","!!","bangbang","exclamation","mark","surprise"]},"exclamation-question-mark":{"a":"Exclamation Question Mark","b":"2049-FE0F","j":["!","!?","?","exclamation","interrobang","mark","punctuation","question","wat","surprise"]},"red-question-mark":{"a":"Red Question Mark","b":"2753-FE0F","j":["?","mark","punctuation","question","question_mark","doubt","confused"]},"white-question-mark":{"a":"White Question Mark","b":"2754","j":["?","mark","outlined","punctuation","question","doubts","gray","huh","confused"]},"white-exclamation-mark":{"a":"White Exclamation Mark","b":"2755","j":["!","exclamation","mark","outlined","punctuation","surprise","gray","wow","warning"]},"red-exclamation-mark":{"a":"Red Exclamation Mark","b":"2757-FE0F","j":["!","exclamation","mark","punctuation","exclamation_mark","heavy_exclamation_mark","danger","surprise","wow","warning"]},"wavy-dash":{"a":"Wavy Dash","b":"3030-FE0F","j":["dash","punctuation","wavy","draw","line","moustache","mustache","squiggle","scribble"]},"currency-exchange":{"a":"Currency Exchange","b":"1F4B1","j":["bank","currency","exchange","money","sales","dollar","travel"]},"heavy-dollar-sign":{"a":"Heavy Dollar Sign","b":"1F4B2","j":["currency","dollar","money","sales","payment","buck"]},"medical-symbol":{"a":"Medical Symbol","b":"2695-FE0F","j":["aesculapius","medicine","staff","health","hospital"]},"recycling-symbol":{"a":"Recycling Symbol","b":"267B-FE0F","j":["recycle","arrow","environment","garbage","trash"]},"fleurdelis":{"a":"Fleur-De-Lis","b":"269C-FE0F","j":["fleur-de-lis","fleur_de_lis","decorative","scout"]},"trident-emblem":{"a":"Trident Emblem","b":"1F531","j":["anchor","emblem","ship","tool","trident","weapon","spear"]},"name-badge":{"a":"Name Badge","b":"1F4DB","j":["badge","name","fire","forbid"]},"japanese-symbol-for-beginner":{"a":"Japanese Symbol for Beginner","b":"1F530","j":["beginner","chevron","Japanese","Japanese symbol for beginner","leaf","badge","shield"]},"hollow-red-circle":{"a":"Hollow Red Circle","b":"2B55-FE0F","j":["circle","large","o","red","round"]},"check-mark-button":{"a":"Check Mark Button","b":"2705","j":["✓","button","check","mark","green-square","ok","agree","vote","election","answer","tick"]},"check-box-with-check":{"a":"Check Box with Check","b":"2611-FE0F","j":["✓","box","check","ok","agree","confirm","black-square","vote","election","yes","tick"]},"check-mark":{"a":"Check Mark","b":"2714-FE0F","j":["✓","check","mark","ok","nike","answer","yes","tick"]},"cross-mark":{"a":"Cross Mark","b":"274C","j":["×","cancel","cross","mark","multiplication","multiply","x","no","delete","remove","red"]},"cross-mark-button":{"a":"Cross Mark Button","b":"274E","j":["×","mark","square","x","green-square","no","deny"]},"curly-loop":{"a":"Curly Loop","b":"27B0","j":["curl","loop","scribble","draw","shape","squiggle"]},"double-curly-loop":{"a":"Double Curly Loop","b":"27BF","j":["curl","double","loop","tape","cassette"]},"part-alternation-mark":{"a":"Part Alternation Mark","b":"303D-FE0F","j":["mark","part","graph","presentation","stats","business","economics","bad"]},"eightspoked-asterisk":{"a":"Eight-Spoked Asterisk","b":"2733-FE0F","j":["*","asterisk","eight-spoked asterisk","eight_spoked_asterisk","star","sparkle","green-square"]},"eightpointed-star":{"a":"Eight-Pointed Star","b":"2734-FE0F","j":["*","eight-pointed star","star","eight_pointed_star","orange-square","shape","polygon"]},"sparkle":{"a":"Sparkle","b":"2747-FE0F","j":["*","stars","green-square","awesome","good","fireworks"]},"copyright":{"a":"Copyright","b":"00A9-FE0F","j":["C","ip","license","circle","law","legal"]},"registered":{"a":"Registered","b":"00AE-FE0F","j":["R","alphabet","circle"]},"trade-mark":{"a":"Trade Mark","b":"2122-FE0F","j":["mark","TM","trademark","brand","law","legal"]},"keycap":{"a":"Keycap: *","b":"002A-FE0F-20E3","j":["keycap_","star"]},"keycap-0":{"a":"Keycap: 0","b":"0030-FE0F-20E3","j":["keycap","0","numbers","blue-square","null"]},"keycap-1":{"a":"Keycap: 1","b":"0031-FE0F-20E3","j":["keycap","blue-square","numbers","1"]},"keycap-2":{"a":"Keycap: 2","b":"0032-FE0F-20E3","j":["keycap","numbers","2","prime","blue-square"]},"keycap-3":{"a":"Keycap: 3","b":"0033-FE0F-20E3","j":["keycap","3","numbers","prime","blue-square"]},"keycap-4":{"a":"Keycap: 4","b":"0034-FE0F-20E3","j":["keycap","4","numbers","blue-square"]},"keycap-5":{"a":"Keycap: 5","b":"0035-FE0F-20E3","j":["keycap","5","numbers","blue-square","prime"]},"keycap-6":{"a":"Keycap: 6","b":"0036-FE0F-20E3","j":["keycap","6","numbers","blue-square"]},"keycap-7":{"a":"Keycap: 7","b":"0037-FE0F-20E3","j":["keycap","7","numbers","blue-square","prime"]},"keycap-8":{"a":"Keycap: 8","b":"0038-FE0F-20E3","j":["keycap","8","blue-square","numbers"]},"keycap-9":{"a":"Keycap: 9","b":"0039-FE0F-20E3","j":["keycap","blue-square","numbers","9"]},"keycap-10":{"a":"Keycap: 10","b":"1F51F","j":["keycap","numbers","10","blue-square"]},"input-latin-uppercase":{"a":"Input Latin Uppercase","b":"1F520","j":["ABCD","input","latin","letters","uppercase","alphabet","words","blue-square"]},"input-latin-lowercase":{"a":"Input Latin Lowercase","b":"1F521","j":["abcd","input","latin","letters","lowercase","blue-square","alphabet"]},"input-numbers":{"a":"Input Numbers","b":"1F522","j":["1234","input","numbers","blue-square","1","2","3","4"]},"input-symbols":{"a":"Input Symbols","b":"1F523","j":["〒♪&%","input","blue-square","music","note","ampersand","percent","glyphs","characters"]},"input-latin-letters":{"a":"Input Latin Letters","b":"1F524","j":["abc","alphabet","input","latin","letters","blue-square"]},"a-button-blood-type":{"a":"A Button (Blood Type)","b":"1F170-FE0F","j":["A","A button (blood type)","blood type","a_button","red-square","alphabet","letter"]},"ab-button-blood-type":{"a":"Ab Button (Blood Type)","b":"1F18E","j":["AB","AB button (blood type)","blood type","ab_button","red-square","alphabet"]},"b-button-blood-type":{"a":"B Button (Blood Type)","b":"1F171-FE0F","j":["B","B button (blood type)","blood type","b_button","red-square","alphabet","letter"]},"cl-button":{"a":"Cl Button","b":"1F191","j":["CL","CL button","alphabet","words","red-square"]},"cool-button":{"a":"Cool Button","b":"1F192","j":["COOL","COOL button","words","blue-square"]},"free-button":{"a":"Free Button","b":"1F193","j":["FREE","FREE button","blue-square","words"]},"information":{"a":"Information","b":"2139-FE0F","j":["i","blue-square","alphabet","letter"]},"id-button":{"a":"Id Button","b":"1F194","j":["ID","ID button","identity","purple-square","words"]},"circled-m":{"a":"Circled M","b":"24C2-FE0F","j":["circle","circled M","M","alphabet","blue-circle","letter"]},"new-button":{"a":"New Button","b":"1F195","j":["NEW","NEW button","blue-square","words","start"]},"ng-button":{"a":"Ng Button","b":"1F196","j":["NG","NG button","blue-square","words","shape","icon"]},"o-button-blood-type":{"a":"O Button (Blood Type)","b":"1F17E-FE0F","j":["blood type","O","O button (blood type)","o_button","alphabet","red-square","letter"]},"ok-button":{"a":"Ok Button","b":"1F197","j":["OK","OK button","good","agree","yes","blue-square"]},"p-button":{"a":"P Button","b":"1F17F-FE0F","j":["P","P button","parking","cars","blue-square","alphabet","letter"]},"sos-button":{"a":"Sos Button","b":"1F198","j":["help","SOS","SOS button","red-square","words","emergency","911"]},"up-button":{"a":"Up! Button","b":"1F199","j":["mark","UP","UP!","UP! button","blue-square","above","high"]},"vs-button":{"a":"Vs Button","b":"1F19A","j":["versus","VS","VS button","words","orange-square"]},"japanese-here-button":{"a":"Japanese “Here” Button","b":"1F201","j":["“here”","Japanese","Japanese “here” button","katakana","ココ","blue-square","here","japanese","destination"]},"japanese-service-charge-button":{"a":"Japanese “Service Charge” Button","b":"1F202-FE0F","j":["“service charge”","Japanese","Japanese “service charge” button","katakana","サ","japanese","blue-square"]},"japanese-monthly-amount-button":{"a":"Japanese “Monthly Amount” Button","b":"1F237-FE0F","j":["“monthly amount”","ideograph","Japanese","Japanese “monthly amount” button","月","chinese","month","moon","japanese","orange-square","kanji"]},"japanese-not-free-of-charge-button":{"a":"Japanese “Not Free of Charge” Button","b":"1F236","j":["“not free of charge”","ideograph","Japanese","Japanese “not free of charge” button","有","orange-square","chinese","have","kanji"]},"japanese-reserved-button":{"a":"Japanese “Reserved” Button","b":"1F22F-FE0F","j":["“reserved”","ideograph","Japanese","Japanese “reserved” button","指","chinese","point","green-square","kanji"]},"japanese-bargain-button":{"a":"Japanese “Bargain” Button","b":"1F250","j":["“bargain”","ideograph","Japanese","Japanese “bargain” button","得","chinese","kanji","obtain","get","circle"]},"japanese-discount-button":{"a":"Japanese “Discount” Button","b":"1F239","j":["“discount”","ideograph","Japanese","Japanese “discount” button","割","cut","divide","chinese","kanji","pink-square"]},"japanese-free-of-charge-button":{"a":"Japanese “Free of Charge” Button","b":"1F21A-FE0F","j":["“free of charge”","ideograph","Japanese","Japanese “free of charge” button","無","nothing","chinese","kanji","japanese","orange-square"]},"japanese-prohibited-button":{"a":"Japanese “Prohibited” Button","b":"1F232","j":["“prohibited”","ideograph","Japanese","Japanese “prohibited” button","禁","kanji","japanese","chinese","forbidden","limit","restricted","red-square"]},"japanese-acceptable-button":{"a":"Japanese “Acceptable” Button","b":"1F251","j":["“acceptable”","ideograph","Japanese","Japanese “acceptable” button","可","ok","good","chinese","kanji","agree","yes","orange-circle"]},"japanese-application-button":{"a":"Japanese “Application” Button","b":"1F238","j":["“application”","ideograph","Japanese","Japanese “application” button","申","chinese","japanese","kanji","orange-square"]},"japanese-passing-grade-button":{"a":"Japanese “Passing Grade” Button","b":"1F234","j":["“passing grade”","ideograph","Japanese","Japanese “passing grade” button","合","japanese","chinese","join","kanji","red-square"]},"japanese-vacancy-button":{"a":"Japanese “Vacancy” Button","b":"1F233","j":["“vacancy”","ideograph","Japanese","Japanese “vacancy” button","空","kanji","japanese","chinese","empty","sky","blue-square"]},"japanese-congratulations-button":{"a":"Japanese “Congratulations” Button","b":"3297-FE0F","j":["“congratulations”","ideograph","Japanese","Japanese “congratulations” button","祝","chinese","kanji","japanese","red-circle"]},"japanese-secret-button":{"a":"Japanese “Secret” Button","b":"3299-FE0F","j":["“secret”","ideograph","Japanese","Japanese “secret” button","秘","privacy","chinese","sshh","kanji","red-circle"]},"japanese-open-for-business-button":{"a":"Japanese “Open for Business” Button","b":"1F23A","j":["“open for business”","ideograph","Japanese","Japanese “open for business” button","営","japanese","opening hours","orange-square"]},"japanese-no-vacancy-button":{"a":"Japanese “No Vacancy” Button","b":"1F235","j":["“no vacancy”","ideograph","Japanese","Japanese “no vacancy” button","満","full","chinese","japanese","red-square","kanji"]},"red-circle":{"a":"Red Circle","b":"1F534","j":["circle","geometric","red","shape","error","danger"]},"orange-circle":{"a":"Orange Circle","b":"1F7E0","j":["circle","orange","round"]},"yellow-circle":{"a":"Yellow Circle","b":"1F7E1","j":["circle","yellow","round"]},"green-circle":{"a":"Green Circle","b":"1F7E2","j":["circle","green","round"]},"blue-circle":{"a":"Blue Circle","b":"1F535","j":["blue","circle","geometric","shape","icon","button"]},"purple-circle":{"a":"Purple Circle","b":"1F7E3","j":["circle","purple","round"]},"brown-circle":{"a":"Brown Circle","b":"1F7E4","j":["brown","circle","round"]},"black-circle":{"a":"Black Circle","b":"26AB-FE0F","j":["circle","geometric","shape","button","round"]},"white-circle":{"a":"White Circle","b":"26AA-FE0F","j":["circle","geometric","shape","round"]},"red-square":{"a":"Red Square","b":"1F7E5","j":["red","square"]},"orange-square":{"a":"Orange Square","b":"1F7E7","j":["orange","square"]},"yellow-square":{"a":"Yellow Square","b":"1F7E8","j":["square","yellow"]},"green-square":{"a":"Green Square","b":"1F7E9","j":["green","square"]},"blue-square":{"a":"Blue Square","b":"1F7E6","j":["blue","square"]},"purple-square":{"a":"Purple Square","b":"1F7EA","j":["purple","square"]},"brown-square":{"a":"Brown Square","b":"1F7EB","j":["brown","square"]},"black-large-square":{"a":"Black Large Square","b":"2B1B-FE0F","j":["geometric","square","shape","icon","button"]},"white-large-square":{"a":"White Large Square","b":"2B1C-FE0F","j":["geometric","square","shape","icon","stone","button"]},"black-medium-square":{"a":"Black Medium Square","b":"25FC-FE0F","j":["geometric","square","shape","button","icon"]},"white-medium-square":{"a":"White Medium Square","b":"25FB-FE0F","j":["geometric","square","shape","stone","icon"]},"black-mediumsmall-square":{"a":"Black Medium-Small Square","b":"25FE-FE0F","j":["black medium-small square","geometric","square","black_medium_small_square","icon","shape","button"]},"white-mediumsmall-square":{"a":"White Medium-Small Square","b":"25FD-FE0F","j":["geometric","square","white medium-small square","white_medium_small_square","shape","stone","icon","button"]},"black-small-square":{"a":"Black Small Square","b":"25AA-FE0F","j":["geometric","square","shape","icon"]},"white-small-square":{"a":"White Small Square","b":"25AB-FE0F","j":["geometric","square","shape","icon"]},"large-orange-diamond":{"a":"Large Orange Diamond","b":"1F536","j":["diamond","geometric","orange","shape","jewel","gem"]},"large-blue-diamond":{"a":"Large Blue Diamond","b":"1F537","j":["blue","diamond","geometric","shape","jewel","gem"]},"small-orange-diamond":{"a":"Small Orange Diamond","b":"1F538","j":["diamond","geometric","orange","shape","jewel","gem"]},"small-blue-diamond":{"a":"Small Blue Diamond","b":"1F539","j":["blue","diamond","geometric","shape","jewel","gem"]},"red-triangle-pointed-up":{"a":"Red Triangle Pointed Up","b":"1F53A","j":["geometric","red","shape","direction","up","top"]},"red-triangle-pointed-down":{"a":"Red Triangle Pointed Down","b":"1F53B","j":["down","geometric","red","shape","direction","bottom"]},"diamond-with-a-dot":{"a":"Diamond with a Dot","b":"1F4A0","j":["comic","diamond","geometric","inside","jewel","blue","gem","crystal","fancy"]},"radio-button":{"a":"Radio Button","b":"1F518","j":["button","geometric","radio","input","old","music","circle"]},"white-square-button":{"a":"White Square Button","b":"1F533","j":["button","geometric","outlined","square","shape","input"]},"black-square-button":{"a":"Black Square Button","b":"1F532","j":["button","geometric","square","shape","input","frame"]},"chequered-flag":{"a":"Chequered Flag","b":"1F3C1","j":["checkered","chequered","racing","contest","finishline","race","gokart"]},"triangular-flag":{"a":"Triangular Flag","b":"1F6A9","j":["post","mark","milestone","place"]},"crossed-flags":{"a":"Crossed Flags","b":"1F38C","j":["celebration","cross","crossed","Japanese","japanese","nation","country","border"]},"black-flag":{"a":"Black Flag","b":"1F3F4","j":["waving","pirate"]},"white-flag":{"a":"White Flag","b":"1F3F3-FE0F","j":["waving","losing","loser","lost","surrender","give up","fail"]},"rainbow-flag":{"a":"Rainbow Flag","b":"1F3F3-FE0F-200D-1F308","j":["pride","rainbow","flag","gay","lgbt","glbt","queer","homosexual","lesbian","bisexual","transgender"]},"transgender-flag":{"a":"Transgender Flag","b":"1F3F3-FE0F-200D-26A7-FE0F","j":["flag","light blue","pink","transgender","white","lgbtq"]},"pirate-flag":{"a":"Pirate Flag","b":"1F3F4-200D-2620-FE0F","j":["Jolly Roger","pirate","plunder","treasure","skull","crossbones","flag","banner"]},"flag-ascension-island":{"a":"Flag: Ascension Island","b":"1F1E6-1F1E8","j":["flag"]},"flag-andorra":{"a":"Flag: Andorra","b":"1F1E6-1F1E9","j":["flag","ad","nation","country","banner","andorra"]},"flag-united-arab-emirates":{"a":"Flag: United Arab Emirates","b":"1F1E6-1F1EA","j":["flag","united","arab","emirates","nation","country","banner","united_arab_emirates"]},"flag-afghanistan":{"a":"Flag: Afghanistan","b":"1F1E6-1F1EB","j":["flag","af","nation","country","banner","afghanistan"]},"flag-antigua--barbuda":{"a":"Flag: Antigua & Barbuda","b":"1F1E6-1F1EC","j":["flag","flag_antigua_barbuda","antigua","barbuda","nation","country","banner","antigua_barbuda"]},"flag-anguilla":{"a":"Flag: Anguilla","b":"1F1E6-1F1EE","j":["flag","ai","nation","country","banner","anguilla"]},"flag-albania":{"a":"Flag: Albania","b":"1F1E6-1F1F1","j":["flag","al","nation","country","banner","albania"]},"flag-armenia":{"a":"Flag: Armenia","b":"1F1E6-1F1F2","j":["flag","am","nation","country","banner","armenia"]},"flag-angola":{"a":"Flag: Angola","b":"1F1E6-1F1F4","j":["flag","ao","nation","country","banner","angola"]},"flag-antarctica":{"a":"Flag: Antarctica","b":"1F1E6-1F1F6","j":["flag","aq","nation","country","banner","antarctica"]},"flag-argentina":{"a":"Flag: Argentina","b":"1F1E6-1F1F7","j":["flag","ar","nation","country","banner","argentina"]},"flag-american-samoa":{"a":"Flag: American Samoa","b":"1F1E6-1F1F8","j":["flag","american","ws","nation","country","banner","american_samoa"]},"flag-austria":{"a":"Flag: Austria","b":"1F1E6-1F1F9","j":["flag","at","nation","country","banner","austria"]},"flag-australia":{"a":"Flag: Australia","b":"1F1E6-1F1FA","j":["flag","au","nation","country","banner","australia"]},"flag-aruba":{"a":"Flag: Aruba","b":"1F1E6-1F1FC","j":["flag","aw","nation","country","banner","aruba"]},"flag-land-islands":{"a":"Flag: Åland Islands","b":"1F1E6-1F1FD","j":["flag","flag_aland_islands","Åland","islands","nation","country","banner","aland_islands"]},"flag-azerbaijan":{"a":"Flag: Azerbaijan","b":"1F1E6-1F1FF","j":["flag","az","nation","country","banner","azerbaijan"]},"flag-bosnia--herzegovina":{"a":"Flag: Bosnia & Herzegovina","b":"1F1E7-1F1E6","j":["flag","flag_bosnia_herzegovina","bosnia","herzegovina","nation","country","banner","bosnia_herzegovina"]},"flag-barbados":{"a":"Flag: Barbados","b":"1F1E7-1F1E7","j":["flag","bb","nation","country","banner","barbados"]},"flag-bangladesh":{"a":"Flag: Bangladesh","b":"1F1E7-1F1E9","j":["flag","bd","nation","country","banner","bangladesh"]},"flag-belgium":{"a":"Flag: Belgium","b":"1F1E7-1F1EA","j":["flag","be","nation","country","banner","belgium"]},"flag-burkina-faso":{"a":"Flag: Burkina Faso","b":"1F1E7-1F1EB","j":["flag","burkina","faso","nation","country","banner","burkina_faso"]},"flag-bulgaria":{"a":"Flag: Bulgaria","b":"1F1E7-1F1EC","j":["flag","bg","nation","country","banner","bulgaria"]},"flag-bahrain":{"a":"Flag: Bahrain","b":"1F1E7-1F1ED","j":["flag","bh","nation","country","banner","bahrain"]},"flag-burundi":{"a":"Flag: Burundi","b":"1F1E7-1F1EE","j":["flag","bi","nation","country","banner","burundi"]},"flag-benin":{"a":"Flag: Benin","b":"1F1E7-1F1EF","j":["flag","bj","nation","country","banner","benin"]},"flag-st-barthlemy":{"a":"Flag: St. Barthélemy","b":"1F1E7-1F1F1","j":["flag","flag_st_barthelemy","saint","barthélemy","nation","country","banner","st_barthelemy"]},"flag-bermuda":{"a":"Flag: Bermuda","b":"1F1E7-1F1F2","j":["flag","bm","nation","country","banner","bermuda"]},"flag-brunei":{"a":"Flag: Brunei","b":"1F1E7-1F1F3","j":["flag","bn","darussalam","nation","country","banner","brunei"]},"flag-bolivia":{"a":"Flag: Bolivia","b":"1F1E7-1F1F4","j":["flag","bo","nation","country","banner","bolivia"]},"flag-caribbean-netherlands":{"a":"Flag: Caribbean Netherlands","b":"1F1E7-1F1F6","j":["flag","bonaire","nation","country","banner","caribbean_netherlands"]},"flag-brazil":{"a":"Flag: Brazil","b":"1F1E7-1F1F7","j":["flag","br","nation","country","banner","brazil"]},"flag-bahamas":{"a":"Flag: Bahamas","b":"1F1E7-1F1F8","j":["flag","bs","nation","country","banner","bahamas"]},"flag-bhutan":{"a":"Flag: Bhutan","b":"1F1E7-1F1F9","j":["flag","bt","nation","country","banner","bhutan"]},"flag-bouvet-island":{"a":"Flag: Bouvet Island","b":"1F1E7-1F1FB","j":["flag","norway"]},"flag-botswana":{"a":"Flag: Botswana","b":"1F1E7-1F1FC","j":["flag","bw","nation","country","banner","botswana"]},"flag-belarus":{"a":"Flag: Belarus","b":"1F1E7-1F1FE","j":["flag","by","nation","country","banner","belarus"]},"flag-belize":{"a":"Flag: Belize","b":"1F1E7-1F1FF","j":["flag","bz","nation","country","banner","belize"]},"flag-canada":{"a":"Flag: Canada","b":"1F1E8-1F1E6","j":["flag","ca","nation","country","banner","canada"]},"flag-cocos-keeling-islands":{"a":"Flag: Cocos (Keeling) Islands","b":"1F1E8-1F1E8","j":["flag","flag_cocos_islands","cocos","keeling","islands","nation","country","banner","cocos_islands"]},"flag-congo--kinshasa":{"a":"Flag: Congo - Kinshasa","b":"1F1E8-1F1E9","j":["flag","flag_congo_kinshasa","congo","democratic","republic","nation","country","banner","congo_kinshasa"]},"flag-central-african-republic":{"a":"Flag: Central African Republic","b":"1F1E8-1F1EB","j":["flag","central","african","republic","nation","country","banner","central_african_republic"]},"flag-congo--brazzaville":{"a":"Flag: Congo - Brazzaville","b":"1F1E8-1F1EC","j":["flag","flag_congo_brazzaville","congo","nation","country","banner","congo_brazzaville"]},"flag-switzerland":{"a":"Flag: Switzerland","b":"1F1E8-1F1ED","j":["flag","ch","nation","country","banner","switzerland"]},"flag-cte-divoire":{"a":"Flag: Côte D’Ivoire","b":"1F1E8-1F1EE","j":["flag","flag_cote_d_ivoire","ivory","coast","nation","country","banner","cote_d_ivoire"]},"flag-cook-islands":{"a":"Flag: Cook Islands","b":"1F1E8-1F1F0","j":["flag","cook","islands","nation","country","banner","cook_islands"]},"flag-chile":{"a":"Flag: Chile","b":"1F1E8-1F1F1","j":["flag","nation","country","banner","chile"]},"flag-cameroon":{"a":"Flag: Cameroon","b":"1F1E8-1F1F2","j":["flag","cm","nation","country","banner","cameroon"]},"flag-china":{"a":"Flag: China","b":"1F1E8-1F1F3","j":["flag","china","chinese","prc","country","nation","banner"]},"flag-colombia":{"a":"Flag: Colombia","b":"1F1E8-1F1F4","j":["flag","co","nation","country","banner","colombia"]},"flag-clipperton-island":{"a":"Flag: Clipperton Island","b":"1F1E8-1F1F5","j":["flag"]},"flag-costa-rica":{"a":"Flag: Costa Rica","b":"1F1E8-1F1F7","j":["flag","costa","rica","nation","country","banner","costa_rica"]},"flag-cuba":{"a":"Flag: Cuba","b":"1F1E8-1F1FA","j":["flag","cu","nation","country","banner","cuba"]},"flag-cape-verde":{"a":"Flag: Cape Verde","b":"1F1E8-1F1FB","j":["flag","cabo","verde","nation","country","banner","cape_verde"]},"flag-curaao":{"a":"Flag: Curaçao","b":"1F1E8-1F1FC","j":["flag","flag_curacao","curaçao","nation","country","banner","curacao"]},"flag-christmas-island":{"a":"Flag: Christmas Island","b":"1F1E8-1F1FD","j":["flag","christmas","island","nation","country","banner","christmas_island"]},"flag-cyprus":{"a":"Flag: Cyprus","b":"1F1E8-1F1FE","j":["flag","cy","nation","country","banner","cyprus"]},"flag-czechia":{"a":"Flag: Czechia","b":"1F1E8-1F1FF","j":["flag","cz","nation","country","banner","czechia"]},"flag-germany":{"a":"Flag: Germany","b":"1F1E9-1F1EA","j":["flag","german","nation","country","banner","germany"]},"flag-diego-garcia":{"a":"Flag: Diego Garcia","b":"1F1E9-1F1EC","j":["flag"]},"flag-djibouti":{"a":"Flag: Djibouti","b":"1F1E9-1F1EF","j":["flag","dj","nation","country","banner","djibouti"]},"flag-denmark":{"a":"Flag: Denmark","b":"1F1E9-1F1F0","j":["flag","dk","nation","country","banner","denmark"]},"flag-dominica":{"a":"Flag: Dominica","b":"1F1E9-1F1F2","j":["flag","dm","nation","country","banner","dominica"]},"flag-dominican-republic":{"a":"Flag: Dominican Republic","b":"1F1E9-1F1F4","j":["flag","dominican","republic","nation","country","banner","dominican_republic"]},"flag-algeria":{"a":"Flag: Algeria","b":"1F1E9-1F1FF","j":["flag","dz","nation","country","banner","algeria"]},"flag-ceuta--melilla":{"a":"Flag: Ceuta & Melilla","b":"1F1EA-1F1E6","j":["flag","flag_ceuta_melilla"]},"flag-ecuador":{"a":"Flag: Ecuador","b":"1F1EA-1F1E8","j":["flag","ec","nation","country","banner","ecuador"]},"flag-estonia":{"a":"Flag: Estonia","b":"1F1EA-1F1EA","j":["flag","ee","nation","country","banner","estonia"]},"flag-egypt":{"a":"Flag: Egypt","b":"1F1EA-1F1EC","j":["flag","eg","nation","country","banner","egypt"]},"flag-western-sahara":{"a":"Flag: Western Sahara","b":"1F1EA-1F1ED","j":["flag","western","sahara","nation","country","banner","western_sahara"]},"flag-eritrea":{"a":"Flag: Eritrea","b":"1F1EA-1F1F7","j":["flag","er","nation","country","banner","eritrea"]},"flag-spain":{"a":"Flag: Spain","b":"1F1EA-1F1F8","j":["flag","spain","nation","country","banner"]},"flag-ethiopia":{"a":"Flag: Ethiopia","b":"1F1EA-1F1F9","j":["flag","et","nation","country","banner","ethiopia"]},"flag-european-union":{"a":"Flag: European Union","b":"1F1EA-1F1FA","j":["flag","european","union","banner"]},"flag-finland":{"a":"Flag: Finland","b":"1F1EB-1F1EE","j":["flag","fi","nation","country","banner","finland"]},"flag-fiji":{"a":"Flag: Fiji","b":"1F1EB-1F1EF","j":["flag","fj","nation","country","banner","fiji"]},"flag-falkland-islands":{"a":"Flag: Falkland Islands","b":"1F1EB-1F1F0","j":["flag","falkland","islands","malvinas","nation","country","banner","falkland_islands"]},"flag-micronesia":{"a":"Flag: Micronesia","b":"1F1EB-1F1F2","j":["flag","micronesia","federated","states","nation","country","banner"]},"flag-faroe-islands":{"a":"Flag: Faroe Islands","b":"1F1EB-1F1F4","j":["flag","faroe","islands","nation","country","banner","faroe_islands"]},"flag-france":{"a":"Flag: France","b":"1F1EB-1F1F7","j":["flag","banner","nation","france","french","country"]},"flag-gabon":{"a":"Flag: Gabon","b":"1F1EC-1F1E6","j":["flag","ga","nation","country","banner","gabon"]},"flag-united-kingdom":{"a":"Flag: United Kingdom","b":"1F1EC-1F1E7","j":["flag","united","kingdom","great","britain","northern","ireland","nation","country","banner","british","UK","english","england","union jack","united_kingdom"]},"flag-grenada":{"a":"Flag: Grenada","b":"1F1EC-1F1E9","j":["flag","gd","nation","country","banner","grenada"]},"flag-georgia":{"a":"Flag: Georgia","b":"1F1EC-1F1EA","j":["flag","ge","nation","country","banner","georgia"]},"flag-french-guiana":{"a":"Flag: French Guiana","b":"1F1EC-1F1EB","j":["flag","french","guiana","nation","country","banner","french_guiana"]},"flag-guernsey":{"a":"Flag: Guernsey","b":"1F1EC-1F1EC","j":["flag","gg","nation","country","banner","guernsey"]},"flag-ghana":{"a":"Flag: Ghana","b":"1F1EC-1F1ED","j":["flag","gh","nation","country","banner","ghana"]},"flag-gibraltar":{"a":"Flag: Gibraltar","b":"1F1EC-1F1EE","j":["flag","gi","nation","country","banner","gibraltar"]},"flag-greenland":{"a":"Flag: Greenland","b":"1F1EC-1F1F1","j":["flag","gl","nation","country","banner","greenland"]},"flag-gambia":{"a":"Flag: Gambia","b":"1F1EC-1F1F2","j":["flag","gm","nation","country","banner","gambia"]},"flag-guinea":{"a":"Flag: Guinea","b":"1F1EC-1F1F3","j":["flag","gn","nation","country","banner","guinea"]},"flag-guadeloupe":{"a":"Flag: Guadeloupe","b":"1F1EC-1F1F5","j":["flag","gp","nation","country","banner","guadeloupe"]},"flag-equatorial-guinea":{"a":"Flag: Equatorial Guinea","b":"1F1EC-1F1F6","j":["flag","equatorial","gn","nation","country","banner","equatorial_guinea"]},"flag-greece":{"a":"Flag: Greece","b":"1F1EC-1F1F7","j":["flag","gr","nation","country","banner","greece"]},"flag-south-georgia--south-sandwich-islands":{"a":"Flag: South Georgia & South Sandwich Islands","b":"1F1EC-1F1F8","j":["flag","flag_south_georgia_south_sandwich_islands","south","georgia","sandwich","islands","nation","country","banner","south_georgia_south_sandwich_islands"]},"flag-guatemala":{"a":"Flag: Guatemala","b":"1F1EC-1F1F9","j":["flag","gt","nation","country","banner","guatemala"]},"flag-guam":{"a":"Flag: Guam","b":"1F1EC-1F1FA","j":["flag","gu","nation","country","banner","guam"]},"flag-guineabissau":{"a":"Flag: Guinea-Bissau","b":"1F1EC-1F1FC","j":["flag","flag_guinea_bissau","gw","bissau","nation","country","banner","guinea_bissau"]},"flag-guyana":{"a":"Flag: Guyana","b":"1F1EC-1F1FE","j":["flag","gy","nation","country","banner","guyana"]},"flag-hong-kong-sar-china":{"a":"Flag: Hong Kong Sar China","b":"1F1ED-1F1F0","j":["flag","hong","kong","nation","country","banner","hong_kong_sar_china"]},"flag-heard--mcdonald-islands":{"a":"Flag: Heard & Mcdonald Islands","b":"1F1ED-1F1F2","j":["flag","flag_heard_mcdonald_islands"]},"flag-honduras":{"a":"Flag: Honduras","b":"1F1ED-1F1F3","j":["flag","hn","nation","country","banner","honduras"]},"flag-croatia":{"a":"Flag: Croatia","b":"1F1ED-1F1F7","j":["flag","hr","nation","country","banner","croatia"]},"flag-haiti":{"a":"Flag: Haiti","b":"1F1ED-1F1F9","j":["flag","ht","nation","country","banner","haiti"]},"flag-hungary":{"a":"Flag: Hungary","b":"1F1ED-1F1FA","j":["flag","hu","nation","country","banner","hungary"]},"flag-canary-islands":{"a":"Flag: Canary Islands","b":"1F1EE-1F1E8","j":["flag","canary","islands","nation","country","banner","canary_islands"]},"flag-indonesia":{"a":"Flag: Indonesia","b":"1F1EE-1F1E9","j":["flag","nation","country","banner","indonesia"]},"flag-ireland":{"a":"Flag: Ireland","b":"1F1EE-1F1EA","j":["flag","ie","nation","country","banner","ireland"]},"flag-israel":{"a":"Flag: Israel","b":"1F1EE-1F1F1","j":["flag","il","nation","country","banner","israel"]},"flag-isle-of-man":{"a":"Flag: Isle of Man","b":"1F1EE-1F1F2","j":["flag","isle","man","nation","country","banner","isle_of_man"]},"flag-india":{"a":"Flag: India","b":"1F1EE-1F1F3","j":["flag","in","nation","country","banner","india"]},"flag-british-indian-ocean-territory":{"a":"Flag: British Indian Ocean Territory","b":"1F1EE-1F1F4","j":["flag","british","indian","ocean","territory","nation","country","banner","british_indian_ocean_territory"]},"flag-iraq":{"a":"Flag: Iraq","b":"1F1EE-1F1F6","j":["flag","iq","nation","country","banner","iraq"]},"flag-iran":{"a":"Flag: Iran","b":"1F1EE-1F1F7","j":["flag","iran","islamic","republic","nation","country","banner"]},"flag-iceland":{"a":"Flag: Iceland","b":"1F1EE-1F1F8","j":["flag","is","nation","country","banner","iceland"]},"flag-italy":{"a":"Flag: Italy","b":"1F1EE-1F1F9","j":["flag","italy","nation","country","banner"]},"flag-jersey":{"a":"Flag: Jersey","b":"1F1EF-1F1EA","j":["flag","je","nation","country","banner","jersey"]},"flag-jamaica":{"a":"Flag: Jamaica","b":"1F1EF-1F1F2","j":["flag","jm","nation","country","banner","jamaica"]},"flag-jordan":{"a":"Flag: Jordan","b":"1F1EF-1F1F4","j":["flag","jo","nation","country","banner","jordan"]},"flag-japan":{"a":"Flag: Japan","b":"1F1EF-1F1F5","j":["flag","japanese","nation","country","banner","japan","jp","ja"]},"flag-kenya":{"a":"Flag: Kenya","b":"1F1F0-1F1EA","j":["flag","ke","nation","country","banner","kenya"]},"flag-kyrgyzstan":{"a":"Flag: Kyrgyzstan","b":"1F1F0-1F1EC","j":["flag","kg","nation","country","banner","kyrgyzstan"]},"flag-cambodia":{"a":"Flag: Cambodia","b":"1F1F0-1F1ED","j":["flag","kh","nation","country","banner","cambodia"]},"flag-kiribati":{"a":"Flag: Kiribati","b":"1F1F0-1F1EE","j":["flag","ki","nation","country","banner","kiribati"]},"flag-comoros":{"a":"Flag: Comoros","b":"1F1F0-1F1F2","j":["flag","km","nation","country","banner","comoros"]},"flag-st-kitts--nevis":{"a":"Flag: St. Kitts & Nevis","b":"1F1F0-1F1F3","j":["flag","flag_st_kitts_nevis","saint","kitts","nevis","nation","country","banner","st_kitts_nevis"]},"flag-north-korea":{"a":"Flag: North Korea","b":"1F1F0-1F1F5","j":["flag","north","korea","nation","country","banner","north_korea"]},"flag-south-korea":{"a":"Flag: South Korea","b":"1F1F0-1F1F7","j":["flag","south","korea","nation","country","banner","south_korea"]},"flag-kuwait":{"a":"Flag: Kuwait","b":"1F1F0-1F1FC","j":["flag","kw","nation","country","banner","kuwait"]},"flag-cayman-islands":{"a":"Flag: Cayman Islands","b":"1F1F0-1F1FE","j":["flag","cayman","islands","nation","country","banner","cayman_islands"]},"flag-kazakhstan":{"a":"Flag: Kazakhstan","b":"1F1F0-1F1FF","j":["flag","kz","nation","country","banner","kazakhstan"]},"flag-laos":{"a":"Flag: Laos","b":"1F1F1-1F1E6","j":["flag","lao","democratic","republic","nation","country","banner","laos"]},"flag-lebanon":{"a":"Flag: Lebanon","b":"1F1F1-1F1E7","j":["flag","lb","nation","country","banner","lebanon"]},"flag-st-lucia":{"a":"Flag: St. Lucia","b":"1F1F1-1F1E8","j":["flag","saint","lucia","nation","country","banner","st_lucia"]},"flag-liechtenstein":{"a":"Flag: Liechtenstein","b":"1F1F1-1F1EE","j":["flag","li","nation","country","banner","liechtenstein"]},"flag-sri-lanka":{"a":"Flag: Sri Lanka","b":"1F1F1-1F1F0","j":["flag","sri","lanka","nation","country","banner","sri_lanka"]},"flag-liberia":{"a":"Flag: Liberia","b":"1F1F1-1F1F7","j":["flag","lr","nation","country","banner","liberia"]},"flag-lesotho":{"a":"Flag: Lesotho","b":"1F1F1-1F1F8","j":["flag","ls","nation","country","banner","lesotho"]},"flag-lithuania":{"a":"Flag: Lithuania","b":"1F1F1-1F1F9","j":["flag","lt","nation","country","banner","lithuania"]},"flag-luxembourg":{"a":"Flag: Luxembourg","b":"1F1F1-1F1FA","j":["flag","lu","nation","country","banner","luxembourg"]},"flag-latvia":{"a":"Flag: Latvia","b":"1F1F1-1F1FB","j":["flag","lv","nation","country","banner","latvia"]},"flag-libya":{"a":"Flag: Libya","b":"1F1F1-1F1FE","j":["flag","ly","nation","country","banner","libya"]},"flag-morocco":{"a":"Flag: Morocco","b":"1F1F2-1F1E6","j":["flag","ma","nation","country","banner","morocco"]},"flag-monaco":{"a":"Flag: Monaco","b":"1F1F2-1F1E8","j":["flag","mc","nation","country","banner","monaco"]},"flag-moldova":{"a":"Flag: Moldova","b":"1F1F2-1F1E9","j":["flag","moldova","republic","nation","country","banner"]},"flag-montenegro":{"a":"Flag: Montenegro","b":"1F1F2-1F1EA","j":["flag","me","nation","country","banner","montenegro"]},"flag-st-martin":{"a":"Flag: St. Martin","b":"1F1F2-1F1EB","j":["flag"]},"flag-madagascar":{"a":"Flag: Madagascar","b":"1F1F2-1F1EC","j":["flag","mg","nation","country","banner","madagascar"]},"flag-marshall-islands":{"a":"Flag: Marshall Islands","b":"1F1F2-1F1ED","j":["flag","marshall","islands","nation","country","banner","marshall_islands"]},"flag-north-macedonia":{"a":"Flag: North Macedonia","b":"1F1F2-1F1F0","j":["flag","macedonia","nation","country","banner","north_macedonia"]},"flag-mali":{"a":"Flag: Mali","b":"1F1F2-1F1F1","j":["flag","ml","nation","country","banner","mali"]},"flag-myanmar-burma":{"a":"Flag: Myanmar (Burma)","b":"1F1F2-1F1F2","j":["flag","flag_myanmar","mm","nation","country","banner","myanmar"]},"flag-mongolia":{"a":"Flag: Mongolia","b":"1F1F2-1F1F3","j":["flag","mn","nation","country","banner","mongolia"]},"flag-macao-sar-china":{"a":"Flag: Macao Sar China","b":"1F1F2-1F1F4","j":["flag","macao","nation","country","banner","macao_sar_china"]},"flag-northern-mariana-islands":{"a":"Flag: Northern Mariana Islands","b":"1F1F2-1F1F5","j":["flag","northern","mariana","islands","nation","country","banner","northern_mariana_islands"]},"flag-martinique":{"a":"Flag: Martinique","b":"1F1F2-1F1F6","j":["flag","mq","nation","country","banner","martinique"]},"flag-mauritania":{"a":"Flag: Mauritania","b":"1F1F2-1F1F7","j":["flag","mr","nation","country","banner","mauritania"]},"flag-montserrat":{"a":"Flag: Montserrat","b":"1F1F2-1F1F8","j":["flag","ms","nation","country","banner","montserrat"]},"flag-malta":{"a":"Flag: Malta","b":"1F1F2-1F1F9","j":["flag","mt","nation","country","banner","malta"]},"flag-mauritius":{"a":"Flag: Mauritius","b":"1F1F2-1F1FA","j":["flag","mu","nation","country","banner","mauritius"]},"flag-maldives":{"a":"Flag: Maldives","b":"1F1F2-1F1FB","j":["flag","mv","nation","country","banner","maldives"]},"flag-malawi":{"a":"Flag: Malawi","b":"1F1F2-1F1FC","j":["flag","mw","nation","country","banner","malawi"]},"flag-mexico":{"a":"Flag: Mexico","b":"1F1F2-1F1FD","j":["flag","mx","nation","country","banner","mexico"]},"flag-malaysia":{"a":"Flag: Malaysia","b":"1F1F2-1F1FE","j":["flag","my","nation","country","banner","malaysia"]},"flag-mozambique":{"a":"Flag: Mozambique","b":"1F1F2-1F1FF","j":["flag","mz","nation","country","banner","mozambique"]},"flag-namibia":{"a":"Flag: Namibia","b":"1F1F3-1F1E6","j":["flag","na","nation","country","banner","namibia"]},"flag-new-caledonia":{"a":"Flag: New Caledonia","b":"1F1F3-1F1E8","j":["flag","new","caledonia","nation","country","banner","new_caledonia"]},"flag-niger":{"a":"Flag: Niger","b":"1F1F3-1F1EA","j":["flag","ne","nation","country","banner","niger"]},"flag-norfolk-island":{"a":"Flag: Norfolk Island","b":"1F1F3-1F1EB","j":["flag","norfolk","island","nation","country","banner","norfolk_island"]},"flag-nigeria":{"a":"Flag: Nigeria","b":"1F1F3-1F1EC","j":["flag","nation","country","banner","nigeria"]},"flag-nicaragua":{"a":"Flag: Nicaragua","b":"1F1F3-1F1EE","j":["flag","ni","nation","country","banner","nicaragua"]},"flag-netherlands":{"a":"Flag: Netherlands","b":"1F1F3-1F1F1","j":["flag","nl","nation","country","banner","netherlands"]},"flag-norway":{"a":"Flag: Norway","b":"1F1F3-1F1F4","j":["flag","no","nation","country","banner","norway"]},"flag-nepal":{"a":"Flag: Nepal","b":"1F1F3-1F1F5","j":["flag","np","nation","country","banner","nepal"]},"flag-nauru":{"a":"Flag: Nauru","b":"1F1F3-1F1F7","j":["flag","nr","nation","country","banner","nauru"]},"flag-niue":{"a":"Flag: Niue","b":"1F1F3-1F1FA","j":["flag","nu","nation","country","banner","niue"]},"flag-new-zealand":{"a":"Flag: New Zealand","b":"1F1F3-1F1FF","j":["flag","new","zealand","nation","country","banner","new_zealand"]},"flag-oman":{"a":"Flag: Oman","b":"1F1F4-1F1F2","j":["flag","om_symbol","nation","country","banner","oman"]},"flag-panama":{"a":"Flag: Panama","b":"1F1F5-1F1E6","j":["flag","pa","nation","country","banner","panama"]},"flag-peru":{"a":"Flag: Peru","b":"1F1F5-1F1EA","j":["flag","pe","nation","country","banner","peru"]},"flag-french-polynesia":{"a":"Flag: French Polynesia","b":"1F1F5-1F1EB","j":["flag","french","polynesia","nation","country","banner","french_polynesia"]},"flag-papua-new-guinea":{"a":"Flag: Papua New Guinea","b":"1F1F5-1F1EC","j":["flag","papua","new","guinea","nation","country","banner","papua_new_guinea"]},"flag-philippines":{"a":"Flag: Philippines","b":"1F1F5-1F1ED","j":["flag","ph","nation","country","banner","philippines"]},"flag-pakistan":{"a":"Flag: Pakistan","b":"1F1F5-1F1F0","j":["flag","pk","nation","country","banner","pakistan"]},"flag-poland":{"a":"Flag: Poland","b":"1F1F5-1F1F1","j":["flag","pl","nation","country","banner","poland"]},"flag-st-pierre--miquelon":{"a":"Flag: St. Pierre & Miquelon","b":"1F1F5-1F1F2","j":["flag","flag_st_pierre_miquelon","saint","pierre","miquelon","nation","country","banner","st_pierre_miquelon"]},"flag-pitcairn-islands":{"a":"Flag: Pitcairn Islands","b":"1F1F5-1F1F3","j":["flag","pitcairn","nation","country","banner","pitcairn_islands"]},"flag-puerto-rico":{"a":"Flag: Puerto Rico","b":"1F1F5-1F1F7","j":["flag","puerto","rico","nation","country","banner","puerto_rico"]},"flag-palestinian-territories":{"a":"Flag: Palestinian Territories","b":"1F1F5-1F1F8","j":["flag","palestine","palestinian","territories","nation","country","banner","palestinian_territories"]},"flag-portugal":{"a":"Flag: Portugal","b":"1F1F5-1F1F9","j":["flag","pt","nation","country","banner","portugal"]},"flag-palau":{"a":"Flag: Palau","b":"1F1F5-1F1FC","j":["flag","pw","nation","country","banner","palau"]},"flag-paraguay":{"a":"Flag: Paraguay","b":"1F1F5-1F1FE","j":["flag","py","nation","country","banner","paraguay"]},"flag-qatar":{"a":"Flag: Qatar","b":"1F1F6-1F1E6","j":["flag","qa","nation","country","banner","qatar"]},"flag-runion":{"a":"Flag: Réunion","b":"1F1F7-1F1EA","j":["flag","flag_reunion","réunion","nation","country","banner","reunion"]},"flag-romania":{"a":"Flag: Romania","b":"1F1F7-1F1F4","j":["flag","ro","nation","country","banner","romania"]},"flag-serbia":{"a":"Flag: Serbia","b":"1F1F7-1F1F8","j":["flag","rs","nation","country","banner","serbia"]},"flag-russia":{"a":"Flag: Russia","b":"1F1F7-1F1FA","j":["flag","russian","federation","nation","country","banner","russia"]},"flag-rwanda":{"a":"Flag: Rwanda","b":"1F1F7-1F1FC","j":["flag","rw","nation","country","banner","rwanda"]},"flag-saudi-arabia":{"a":"Flag: Saudi Arabia","b":"1F1F8-1F1E6","j":["flag","nation","country","banner","saudi_arabia"]},"flag-solomon-islands":{"a":"Flag: Solomon Islands","b":"1F1F8-1F1E7","j":["flag","solomon","islands","nation","country","banner","solomon_islands"]},"flag-seychelles":{"a":"Flag: Seychelles","b":"1F1F8-1F1E8","j":["flag","sc","nation","country","banner","seychelles"]},"flag-sudan":{"a":"Flag: Sudan","b":"1F1F8-1F1E9","j":["flag","sd","nation","country","banner","sudan"]},"flag-sweden":{"a":"Flag: Sweden","b":"1F1F8-1F1EA","j":["flag","se","nation","country","banner","sweden"]},"flag-singapore":{"a":"Flag: Singapore","b":"1F1F8-1F1EC","j":["flag","sg","nation","country","banner","singapore"]},"flag-st-helena":{"a":"Flag: St. Helena","b":"1F1F8-1F1ED","j":["flag","saint","helena","ascension","tristan","cunha","nation","country","banner","st_helena"]},"flag-slovenia":{"a":"Flag: Slovenia","b":"1F1F8-1F1EE","j":["flag","si","nation","country","banner","slovenia"]},"flag-svalbard--jan-mayen":{"a":"Flag: Svalbard & Jan Mayen","b":"1F1F8-1F1EF","j":["flag","flag_svalbard_jan_mayen"]},"flag-slovakia":{"a":"Flag: Slovakia","b":"1F1F8-1F1F0","j":["flag","sk","nation","country","banner","slovakia"]},"flag-sierra-leone":{"a":"Flag: Sierra Leone","b":"1F1F8-1F1F1","j":["flag","sierra","leone","nation","country","banner","sierra_leone"]},"flag-san-marino":{"a":"Flag: San Marino","b":"1F1F8-1F1F2","j":["flag","san","marino","nation","country","banner","san_marino"]},"flag-senegal":{"a":"Flag: Senegal","b":"1F1F8-1F1F3","j":["flag","sn","nation","country","banner","senegal"]},"flag-somalia":{"a":"Flag: Somalia","b":"1F1F8-1F1F4","j":["flag","so","nation","country","banner","somalia"]},"flag-suriname":{"a":"Flag: Suriname","b":"1F1F8-1F1F7","j":["flag","sr","nation","country","banner","suriname"]},"flag-south-sudan":{"a":"Flag: South Sudan","b":"1F1F8-1F1F8","j":["flag","south","sd","nation","country","banner","south_sudan"]},"flag-so-tom--prncipe":{"a":"Flag: São Tomé & Príncipe","b":"1F1F8-1F1F9","j":["flag","flag_sao_tome_principe","sao","tome","principe","nation","country","banner","sao_tome_principe"]},"flag-el-salvador":{"a":"Flag: El Salvador","b":"1F1F8-1F1FB","j":["flag","el","salvador","nation","country","banner","el_salvador"]},"flag-sint-maarten":{"a":"Flag: Sint Maarten","b":"1F1F8-1F1FD","j":["flag","sint","maarten","dutch","nation","country","banner","sint_maarten"]},"flag-syria":{"a":"Flag: Syria","b":"1F1F8-1F1FE","j":["flag","syrian","arab","republic","nation","country","banner","syria"]},"flag-eswatini":{"a":"Flag: Eswatini","b":"1F1F8-1F1FF","j":["flag","sz","nation","country","banner","eswatini"]},"flag-tristan-da-cunha":{"a":"Flag: Tristan Da Cunha","b":"1F1F9-1F1E6","j":["flag"]},"flag-turks--caicos-islands":{"a":"Flag: Turks & Caicos Islands","b":"1F1F9-1F1E8","j":["flag","flag_turks_caicos_islands","turks","caicos","islands","nation","country","banner","turks_caicos_islands"]},"flag-chad":{"a":"Flag: Chad","b":"1F1F9-1F1E9","j":["flag","td","nation","country","banner","chad"]},"flag-french-southern-territories":{"a":"Flag: French Southern Territories","b":"1F1F9-1F1EB","j":["flag","french","southern","territories","nation","country","banner","french_southern_territories"]},"flag-togo":{"a":"Flag: Togo","b":"1F1F9-1F1EC","j":["flag","tg","nation","country","banner","togo"]},"flag-thailand":{"a":"Flag: Thailand","b":"1F1F9-1F1ED","j":["flag","th","nation","country","banner","thailand"]},"flag-tajikistan":{"a":"Flag: Tajikistan","b":"1F1F9-1F1EF","j":["flag","tj","nation","country","banner","tajikistan"]},"flag-tokelau":{"a":"Flag: Tokelau","b":"1F1F9-1F1F0","j":["flag","tk","nation","country","banner","tokelau"]},"flag-timorleste":{"a":"Flag: Timor-Leste","b":"1F1F9-1F1F1","j":["flag","flag_timor_leste","timor","leste","nation","country","banner","timor_leste"]},"flag-turkmenistan":{"a":"Flag: Turkmenistan","b":"1F1F9-1F1F2","j":["flag","nation","country","banner","turkmenistan"]},"flag-tunisia":{"a":"Flag: Tunisia","b":"1F1F9-1F1F3","j":["flag","tn","nation","country","banner","tunisia"]},"flag-tonga":{"a":"Flag: Tonga","b":"1F1F9-1F1F4","j":["flag","to","nation","country","banner","tonga"]},"flag-turkey":{"a":"Flag: Turkey","b":"1F1F9-1F1F7","j":["flag","turkey","nation","country","banner"]},"flag-trinidad--tobago":{"a":"Flag: Trinidad & Tobago","b":"1F1F9-1F1F9","j":["flag","flag_trinidad_tobago","trinidad","tobago","nation","country","banner","trinidad_tobago"]},"flag-tuvalu":{"a":"Flag: Tuvalu","b":"1F1F9-1F1FB","j":["flag","nation","country","banner","tuvalu"]},"flag-taiwan":{"a":"Flag: Taiwan","b":"1F1F9-1F1FC","j":["flag","tw","nation","country","banner","taiwan"]},"flag-tanzania":{"a":"Flag: Tanzania","b":"1F1F9-1F1FF","j":["flag","tanzania","united","republic","nation","country","banner"]},"flag-ukraine":{"a":"Flag: Ukraine","b":"1F1FA-1F1E6","j":["flag","ua","nation","country","banner","ukraine"]},"flag-uganda":{"a":"Flag: Uganda","b":"1F1FA-1F1EC","j":["flag","ug","nation","country","banner","uganda"]},"flag-us-outlying-islands":{"a":"Flag: U.S. Outlying Islands","b":"1F1FA-1F1F2","j":["flag","flag_u_s_outlying_islands"]},"flag-united-nations":{"a":"Flag: United Nations","b":"1F1FA-1F1F3","j":["flag","un","banner"]},"flag-united-states":{"a":"Flag: United States","b":"1F1FA-1F1F8","j":["flag","united","states","america","nation","country","banner","united_states"]},"flag-uruguay":{"a":"Flag: Uruguay","b":"1F1FA-1F1FE","j":["flag","uy","nation","country","banner","uruguay"]},"flag-uzbekistan":{"a":"Flag: Uzbekistan","b":"1F1FA-1F1FF","j":["flag","uz","nation","country","banner","uzbekistan"]},"flag-vatican-city":{"a":"Flag: Vatican City","b":"1F1FB-1F1E6","j":["flag","vatican","city","nation","country","banner","vatican_city"]},"flag-st-vincent--grenadines":{"a":"Flag: St. Vincent & Grenadines","b":"1F1FB-1F1E8","j":["flag","flag_st_vincent_grenadines","saint","vincent","grenadines","nation","country","banner","st_vincent_grenadines"]},"flag-venezuela":{"a":"Flag: Venezuela","b":"1F1FB-1F1EA","j":["flag","ve","bolivarian","republic","nation","country","banner","venezuela"]},"flag-british-virgin-islands":{"a":"Flag: British Virgin Islands","b":"1F1FB-1F1EC","j":["flag","british","virgin","islands","bvi","nation","country","banner","british_virgin_islands"]},"flag-us-virgin-islands":{"a":"Flag: U.S. Virgin Islands","b":"1F1FB-1F1EE","j":["flag","flag_u_s_virgin_islands","virgin","islands","us","nation","country","banner","u_s_virgin_islands"]},"flag-vietnam":{"a":"Flag: Vietnam","b":"1F1FB-1F1F3","j":["flag","viet","nam","nation","country","banner","vietnam"]},"flag-vanuatu":{"a":"Flag: Vanuatu","b":"1F1FB-1F1FA","j":["flag","vu","nation","country","banner","vanuatu"]},"flag-wallis--futuna":{"a":"Flag: Wallis & Futuna","b":"1F1FC-1F1EB","j":["flag","flag_wallis_futuna","wallis","futuna","nation","country","banner","wallis_futuna"]},"flag-samoa":{"a":"Flag: Samoa","b":"1F1FC-1F1F8","j":["flag","ws","nation","country","banner","samoa"]},"flag-kosovo":{"a":"Flag: Kosovo","b":"1F1FD-1F1F0","j":["flag","xk","nation","country","banner","kosovo"]},"flag-yemen":{"a":"Flag: Yemen","b":"1F1FE-1F1EA","j":["flag","ye","nation","country","banner","yemen"]},"flag-mayotte":{"a":"Flag: Mayotte","b":"1F1FE-1F1F9","j":["flag","yt","nation","country","banner","mayotte"]},"flag-south-africa":{"a":"Flag: South Africa","b":"1F1FF-1F1E6","j":["flag","south","africa","nation","country","banner","south_africa"]},"flag-zambia":{"a":"Flag: Zambia","b":"1F1FF-1F1F2","j":["flag","zm","nation","country","banner","zambia"]},"flag-zimbabwe":{"a":"Flag: Zimbabwe","b":"1F1FF-1F1FC","j":["flag","zw","nation","country","banner","zimbabwe"]},"flag-england":{"a":"Flag: England","b":"1F3F4-E0067-E0062-E0065-E006E-E0067-E007F","j":["flag","english"]},"flag-scotland":{"a":"Flag: Scotland","b":"1F3F4-E0067-E0062-E0073-E0063-E0074-E007F","j":["flag","scottish"]},"flag-wales":{"a":"Flag: Wales","b":"1F3F4-E0067-E0062-E0077-E006C-E0073-E007F","j":["flag","welsh"]}},"aliases":{}} \ No newline at end of file +{"compressed":true,"categories":[{"id":"smileys_&_emotion","name":"Smileys & Emotion","emojis":["grinning-face","grinning-face-with-big-eyes","grinning-face-with-smiling-eyes","beaming-face-with-smiling-eyes","grinning-squinting-face","grinning-face-with-sweat","rolling-on-the-floor-laughing","face-with-tears-of-joy","slightly-smiling-face","upsidedown-face","melting-face","winking-face","smiling-face-with-smiling-eyes","smiling-face-with-halo","smiling-face-with-hearts","smiling-face-with-hearteyes","starstruck","face-blowing-a-kiss","kissing-face","smiling-face","kissing-face-with-closed-eyes","kissing-face-with-smiling-eyes","smiling-face-with-tear","face-savoring-food","face-with-tongue","winking-face-with-tongue","zany-face","squinting-face-with-tongue","moneymouth-face","smiling-face-with-open-hands","face-with-hand-over-mouth","face-with-open-eyes-and-hand-over-mouth","face-with-peeking-eye","shushing-face","thinking-face","saluting-face","zippermouth-face","face-with-raised-eyebrow","neutral-face","expressionless-face","face-without-mouth","dotted-line-face","face-in-clouds","smirking-face","unamused-face","face-with-rolling-eyes","grimacing-face","face-exhaling","lying-face","shaking-face","head-shaking-horizontally","head-shaking-vertically","relieved-face","pensive-face","sleepy-face","drooling-face","sleeping-face","face-with-medical-mask","face-with-thermometer","face-with-headbandage","nauseated-face","face-vomiting","sneezing-face","hot-face","cold-face","woozy-face","face-with-crossedout-eyes","face-with-spiral-eyes","exploding-head","cowboy-hat-face","partying-face","disguised-face","smiling-face-with-sunglasses","nerd-face","face-with-monocle","confused-face","face-with-diagonal-mouth","worried-face","slightly-frowning-face","frowning-face","face-with-open-mouth","hushed-face","astonished-face","flushed-face","pleading-face","face-holding-back-tears","frowning-face-with-open-mouth","anguished-face","fearful-face","anxious-face-with-sweat","sad-but-relieved-face","crying-face","loudly-crying-face","face-screaming-in-fear","confounded-face","persevering-face","disappointed-face","downcast-face-with-sweat","weary-face","tired-face","yawning-face","face-with-steam-from-nose","enraged-face","angry-face","face-with-symbols-on-mouth","smiling-face-with-horns","angry-face-with-horns","skull","skull-and-crossbones","pile-of-poo","clown-face","ogre","goblin","ghost","alien","alien-monster","robot","grinning-cat","grinning-cat-with-smiling-eyes","cat-with-tears-of-joy","smiling-cat-with-hearteyes","cat-with-wry-smile","kissing-cat","weary-cat","crying-cat","pouting-cat","seenoevil-monkey","hearnoevil-monkey","speaknoevil-monkey","love-letter","heart-with-arrow","heart-with-ribbon","sparkling-heart","growing-heart","beating-heart","revolving-hearts","two-hearts","heart-decoration","heart-exclamation","broken-heart","heart-on-fire","mending-heart","red-heart","pink-heart","orange-heart","yellow-heart","green-heart","blue-heart","light-blue-heart","purple-heart","brown-heart","black-heart","grey-heart","white-heart","kiss-mark","hundred-points","anger-symbol","collision","dizzy","sweat-droplets","dashing-away","hole","speech-balloon","eye-in-speech-bubble","left-speech-bubble","right-anger-bubble","thought-balloon","zzz"]},{"id":"people_&_body","name":"People & Body","emojis":["waving-hand","raised-back-of-hand","hand-with-fingers-splayed","raised-hand","vulcan-salute","rightwards-hand","leftwards-hand","palm-down-hand","palm-up-hand","leftwards-pushing-hand","rightwards-pushing-hand","ok-hand","pinched-fingers","pinching-hand","victory-hand","crossed-fingers","hand-with-index-finger-and-thumb-crossed","loveyou-gesture","sign-of-the-horns","call-me-hand","backhand-index-pointing-left","backhand-index-pointing-right","backhand-index-pointing-up","middle-finger","backhand-index-pointing-down","index-pointing-up","index-pointing-at-the-viewer","thumbs-up","thumbs-down","raised-fist","oncoming-fist","leftfacing-fist","rightfacing-fist","clapping-hands","raising-hands","heart-hands","open-hands","palms-up-together","handshake","folded-hands","writing-hand","nail-polish","selfie","flexed-biceps","mechanical-arm","mechanical-leg","leg","foot","ear","ear-with-hearing-aid","nose","brain","anatomical-heart","lungs","tooth","bone","eyes","eye","tongue","mouth","biting-lip","baby","child","boy","girl","person","person-blond-hair","man","person-beard","man-beard","woman-beard","man-red-hair","man-curly-hair","man-white-hair","man-bald","woman","woman-red-hair","person-red-hair","woman-curly-hair","person-curly-hair","woman-white-hair","person-white-hair","woman-bald","person-bald","woman-blond-hair","man-blond-hair","older-person","old-man","old-woman","person-frowning","man-frowning","woman-frowning","person-pouting","man-pouting","woman-pouting","person-gesturing-no","man-gesturing-no","woman-gesturing-no","person-gesturing-ok","man-gesturing-ok","woman-gesturing-ok","person-tipping-hand","man-tipping-hand","woman-tipping-hand","person-raising-hand","man-raising-hand","woman-raising-hand","deaf-person","deaf-man","deaf-woman","person-bowing","man-bowing","woman-bowing","person-facepalming","man-facepalming","woman-facepalming","person-shrugging","man-shrugging","woman-shrugging","health-worker","man-health-worker","woman-health-worker","student","man-student","woman-student","teacher","man-teacher","woman-teacher","judge","man-judge","woman-judge","farmer","man-farmer","woman-farmer","cook","man-cook","woman-cook","mechanic","man-mechanic","woman-mechanic","factory-worker","man-factory-worker","woman-factory-worker","office-worker","man-office-worker","woman-office-worker","scientist","man-scientist","woman-scientist","technologist","man-technologist","woman-technologist","singer","man-singer","woman-singer","artist","man-artist","woman-artist","pilot","man-pilot","woman-pilot","astronaut","man-astronaut","woman-astronaut","firefighter","man-firefighter","woman-firefighter","police-officer","man-police-officer","woman-police-officer","detective","man-detective","woman-detective","guard","man-guard","woman-guard","ninja","construction-worker","man-construction-worker","woman-construction-worker","person-with-crown","prince","princess","person-wearing-turban","man-wearing-turban","woman-wearing-turban","person-with-skullcap","woman-with-headscarf","person-in-tuxedo","man-in-tuxedo","woman-in-tuxedo","person-with-veil","man-with-veil","woman-with-veil","pregnant-woman","pregnant-man","pregnant-person","breastfeeding","woman-feeding-baby","man-feeding-baby","person-feeding-baby","baby-angel","santa-claus","mrs-claus","mx-claus","superhero","man-superhero","woman-superhero","supervillain","man-supervillain","woman-supervillain","mage","man-mage","woman-mage","fairy","man-fairy","woman-fairy","vampire","man-vampire","woman-vampire","merperson","merman","mermaid","elf","man-elf","woman-elf","genie","man-genie","woman-genie","zombie","man-zombie","woman-zombie","troll","person-getting-massage","man-getting-massage","woman-getting-massage","person-getting-haircut","man-getting-haircut","woman-getting-haircut","person-walking","man-walking","woman-walking","person-walking-facing-right","woman-walking-facing-right","man-walking-facing-right","person-standing","man-standing","woman-standing","person-kneeling","man-kneeling","woman-kneeling","person-kneeling-facing-right","woman-kneeling-facing-right","man-kneeling-facing-right","person-with-white-cane","person-with-white-cane-facing-right","man-with-white-cane","man-with-white-cane-facing-right","woman-with-white-cane","woman-with-white-cane-facing-right","person-in-motorized-wheelchair","person-in-motorized-wheelchair-facing-right","man-in-motorized-wheelchair","man-in-motorized-wheelchair-facing-right","woman-in-motorized-wheelchair","woman-in-motorized-wheelchair-facing-right","person-in-manual-wheelchair","person-in-manual-wheelchair-facing-right","man-in-manual-wheelchair","man-in-manual-wheelchair-facing-right","woman-in-manual-wheelchair","woman-in-manual-wheelchair-facing-right","person-running","man-running","woman-running","person-running-facing-right","woman-running-facing-right","man-running-facing-right","woman-dancing","man-dancing","person-in-suit-levitating","people-with-bunny-ears","men-with-bunny-ears","women-with-bunny-ears","person-in-steamy-room","man-in-steamy-room","woman-in-steamy-room","person-climbing","man-climbing","woman-climbing","person-fencing","horse-racing","skier","snowboarder","person-golfing","man-golfing","woman-golfing","person-surfing","man-surfing","woman-surfing","person-rowing-boat","man-rowing-boat","woman-rowing-boat","person-swimming","man-swimming","woman-swimming","person-bouncing-ball","man-bouncing-ball","woman-bouncing-ball","person-lifting-weights","man-lifting-weights","woman-lifting-weights","person-biking","man-biking","woman-biking","person-mountain-biking","man-mountain-biking","woman-mountain-biking","person-cartwheeling","man-cartwheeling","woman-cartwheeling","people-wrestling","men-wrestling","women-wrestling","person-playing-water-polo","man-playing-water-polo","woman-playing-water-polo","person-playing-handball","man-playing-handball","woman-playing-handball","person-juggling","man-juggling","woman-juggling","person-in-lotus-position","man-in-lotus-position","woman-in-lotus-position","person-taking-bath","person-in-bed","people-holding-hands","women-holding-hands","woman-and-man-holding-hands","men-holding-hands","kiss","kiss-woman-man","kiss-man-man","kiss-woman-woman","couple-with-heart","couple-with-heart-woman-man","couple-with-heart-man-man","couple-with-heart-woman-woman","family-man-woman-boy","family-man-woman-girl","family-man-woman-girl-boy","family-man-woman-boy-boy","family-man-woman-girl-girl","family-man-man-boy","family-man-man-girl","family-man-man-girl-boy","family-man-man-boy-boy","family-man-man-girl-girl","family-woman-woman-boy","family-woman-woman-girl","family-woman-woman-girl-boy","family-woman-woman-boy-boy","family-woman-woman-girl-girl","family-man-boy","family-man-boy-boy","family-man-girl","family-man-girl-boy","family-man-girl-girl","family-woman-boy","family-woman-boy-boy","family-woman-girl","family-woman-girl-boy","family-woman-girl-girl","speaking-head","bust-in-silhouette","busts-in-silhouette","people-hugging","family","family-adult-adult-child","family-adult-adult-child-child","family-adult-child","family-adult-child-child","footprints"]},{"id":"animals_&_nature","name":"Animals & Nature","emojis":["monkey-face","monkey","gorilla","orangutan","dog-face","dog","guide-dog","service-dog","poodle","wolf","fox","raccoon","cat-face","cat","black-cat","lion","tiger-face","tiger","leopard","horse-face","moose","donkey","horse","unicorn","zebra","deer","bison","cow-face","ox","water-buffalo","cow","pig-face","pig","boar","pig-nose","ram","ewe","goat","camel","twohump-camel","llama","giraffe","elephant","mammoth","rhinoceros","hippopotamus","mouse-face","mouse","rat","hamster","rabbit-face","rabbit","chipmunk","beaver","hedgehog","bat","bear","polar-bear","koala","panda","sloth","otter","skunk","kangaroo","badger","paw-prints","turkey","chicken","rooster","hatching-chick","baby-chick","frontfacing-baby-chick","bird","penguin","dove","eagle","duck","swan","owl","dodo","feather","flamingo","peacock","parrot","wing","black-bird","goose","phoenix","frog","crocodile","turtle","lizard","snake","dragon-face","dragon","sauropod","trex","spouting-whale","whale","dolphin","seal","fish","tropical-fish","blowfish","shark","octopus","spiral-shell","coral","jellyfish","snail","butterfly","bug","ant","honeybee","beetle","lady-beetle","cricket","cockroach","spider","spider-web","scorpion","mosquito","fly","worm","microbe","bouquet","cherry-blossom","white-flower","lotus","rosette","rose","wilted-flower","hibiscus","sunflower","blossom","tulip","hyacinth","seedling","potted-plant","evergreen-tree","deciduous-tree","palm-tree","cactus","sheaf-of-rice","herb","shamrock","four-leaf-clover","maple-leaf","fallen-leaf","leaf-fluttering-in-wind","empty-nest","nest-with-eggs","mushroom"]},{"id":"food_&_drink","name":"Food & Drink","emojis":["grapes","melon","watermelon","tangerine","lemon","lime","banana","pineapple","mango","red-apple","green-apple","pear","peach","cherries","strawberry","blueberries","kiwi-fruit","tomato","olive","coconut","avocado","eggplant","potato","carrot","ear-of-corn","hot-pepper","bell-pepper","cucumber","leafy-green","broccoli","garlic","onion","peanuts","beans","chestnut","ginger-root","pea-pod","brown-mushroom","bread","croissant","baguette-bread","flatbread","pretzel","bagel","pancakes","waffle","cheese-wedge","meat-on-bone","poultry-leg","cut-of-meat","bacon","hamburger","french-fries","pizza","hot-dog","sandwich","taco","burrito","tamale","stuffed-flatbread","falafel","egg","cooking","shallow-pan-of-food","pot-of-food","fondue","bowl-with-spoon","green-salad","popcorn","butter","salt","canned-food","bento-box","rice-cracker","rice-ball","cooked-rice","curry-rice","steaming-bowl","spaghetti","roasted-sweet-potato","oden","sushi","fried-shrimp","fish-cake-with-swirl","moon-cake","dango","dumpling","fortune-cookie","takeout-box","crab","lobster","shrimp","squid","oyster","soft-ice-cream","shaved-ice","ice-cream","doughnut","cookie","birthday-cake","shortcake","cupcake","pie","chocolate-bar","candy","lollipop","custard","honey-pot","baby-bottle","glass-of-milk","hot-beverage","teapot","teacup-without-handle","sake","bottle-with-popping-cork","wine-glass","cocktail-glass","tropical-drink","beer-mug","clinking-beer-mugs","clinking-glasses","tumbler-glass","pouring-liquid","cup-with-straw","bubble-tea","beverage-box","mate","ice","chopsticks","fork-and-knife-with-plate","fork-and-knife","spoon","kitchen-knife","jar","amphora"]},{"id":"travel_&_places","name":"Travel & Places","emojis":["globe-showing-europeafrica","globe-showing-americas","globe-showing-asiaaustralia","globe-with-meridians","world-map","map-of-japan","compass","snowcapped-mountain","mountain","volcano","mount-fuji","camping","beach-with-umbrella","desert","desert-island","national-park","stadium","classical-building","building-construction","brick","rock","wood","hut","houses","derelict-house","house","house-with-garden","office-building","japanese-post-office","post-office","hospital","bank","hotel","love-hotel","convenience-store","school","department-store","factory","japanese-castle","castle","wedding","tokyo-tower","statue-of-liberty","church","mosque","hindu-temple","synagogue","shinto-shrine","kaaba","fountain","tent","foggy","night-with-stars","cityscape","sunrise-over-mountains","sunrise","cityscape-at-dusk","sunset","bridge-at-night","hot-springs","carousel-horse","playground-slide","ferris-wheel","roller-coaster","barber-pole","circus-tent","locomotive","railway-car","highspeed-train","bullet-train","train","metro","light-rail","station","tram","monorail","mountain-railway","tram-car","bus","oncoming-bus","trolleybus","minibus","ambulance","fire-engine","police-car","oncoming-police-car","taxi","oncoming-taxi","automobile","oncoming-automobile","sport-utility-vehicle","pickup-truck","delivery-truck","articulated-lorry","tractor","racing-car","motorcycle","motor-scooter","manual-wheelchair","motorized-wheelchair","auto-rickshaw","bicycle","kick-scooter","skateboard","roller-skate","bus-stop","motorway","railway-track","oil-drum","fuel-pump","wheel","police-car-light","horizontal-traffic-light","vertical-traffic-light","stop-sign","construction","anchor","ring-buoy","sailboat","canoe","speedboat","passenger-ship","ferry","motor-boat","ship","airplane","small-airplane","airplane-departure","airplane-arrival","parachute","seat","helicopter","suspension-railway","mountain-cableway","aerial-tramway","satellite","rocket","flying-saucer","bellhop-bell","luggage","hourglass-done","hourglass-not-done","watch","alarm-clock","stopwatch","timer-clock","mantelpiece-clock","twelve-oclock","twelvethirty","one-oclock","onethirty","two-oclock","twothirty","three-oclock","threethirty","four-oclock","fourthirty","five-oclock","fivethirty","six-oclock","sixthirty","seven-oclock","seventhirty","eight-oclock","eightthirty","nine-oclock","ninethirty","ten-oclock","tenthirty","eleven-oclock","eleventhirty","new-moon","waxing-crescent-moon","first-quarter-moon","waxing-gibbous-moon","full-moon","waning-gibbous-moon","last-quarter-moon","waning-crescent-moon","crescent-moon","new-moon-face","first-quarter-moon-face","last-quarter-moon-face","thermometer","sun","full-moon-face","sun-with-face","ringed-planet","star","glowing-star","shooting-star","milky-way","cloud","sun-behind-cloud","cloud-with-lightning-and-rain","sun-behind-small-cloud","sun-behind-large-cloud","sun-behind-rain-cloud","cloud-with-rain","cloud-with-snow","cloud-with-lightning","tornado","fog","wind-face","cyclone","rainbow","closed-umbrella","umbrella","umbrella-with-rain-drops","umbrella-on-ground","high-voltage","snowflake","snowman","snowman-without-snow","comet","fire","droplet","water-wave"]},{"id":"activities","name":"Activities","emojis":["jackolantern","christmas-tree","fireworks","sparkler","firecracker","sparkles","balloon","party-popper","confetti-ball","tanabata-tree","pine-decoration","japanese-dolls","carp-streamer","wind-chime","moon-viewing-ceremony","red-envelope","ribbon","wrapped-gift","reminder-ribbon","admission-tickets","ticket","military-medal","trophy","sports-medal","1st-place-medal","2nd-place-medal","3rd-place-medal","soccer-ball","baseball","softball","basketball","volleyball","american-football","rugby-football","tennis","flying-disc","bowling","cricket-game","field-hockey","ice-hockey","lacrosse","ping-pong","badminton","boxing-glove","martial-arts-uniform","goal-net","flag-in-hole","ice-skate","fishing-pole","diving-mask","running-shirt","skis","sled","curling-stone","bullseye","yoyo","kite","water-pistol","pool-8-ball","crystal-ball","magic-wand","video-game","joystick","slot-machine","game-die","puzzle-piece","teddy-bear","piata","mirror-ball","nesting-dolls","spade-suit","heart-suit","diamond-suit","club-suit","chess-pawn","joker","mahjong-red-dragon","flower-playing-cards","performing-arts","framed-picture","artist-palette","thread","sewing-needle","yarn","knot"]},{"id":"objects","name":"Objects","emojis":["glasses","sunglasses","goggles","lab-coat","safety-vest","necktie","tshirt","jeans","scarf","gloves","coat","socks","dress","kimono","sari","onepiece-swimsuit","briefs","shorts","bikini","womans-clothes","folding-hand-fan","purse","handbag","clutch-bag","shopping-bags","backpack","thong-sandal","mans-shoe","running-shoe","hiking-boot","flat-shoe","highheeled-shoe","womans-sandal","ballet-shoes","womans-boot","hair-pick","crown","womans-hat","top-hat","graduation-cap","billed-cap","military-helmet","rescue-workers-helmet","prayer-beads","lipstick","ring","gem-stone","muted-speaker","speaker-low-volume","speaker-medium-volume","speaker-high-volume","loudspeaker","megaphone","postal-horn","bell","bell-with-slash","musical-score","musical-note","musical-notes","studio-microphone","level-slider","control-knobs","microphone","headphone","radio","saxophone","accordion","guitar","musical-keyboard","trumpet","violin","banjo","drum","long-drum","maracas","flute","mobile-phone","mobile-phone-with-arrow","telephone","telephone-receiver","pager","fax-machine","battery","low-battery","electric-plug","laptop","desktop-computer","printer","keyboard","computer-mouse","trackball","computer-disk","floppy-disk","optical-disk","dvd","abacus","movie-camera","film-frames","film-projector","clapper-board","television","camera","camera-with-flash","video-camera","videocassette","magnifying-glass-tilted-left","magnifying-glass-tilted-right","candle","light-bulb","flashlight","red-paper-lantern","diya-lamp","notebook-with-decorative-cover","closed-book","open-book","green-book","blue-book","orange-book","books","notebook","ledger","page-with-curl","scroll","page-facing-up","newspaper","rolledup-newspaper","bookmark-tabs","bookmark","label","money-bag","coin","yen-banknote","dollar-banknote","euro-banknote","pound-banknote","money-with-wings","credit-card","receipt","chart-increasing-with-yen","envelope","email","incoming-envelope","envelope-with-arrow","outbox-tray","inbox-tray","package","closed-mailbox-with-raised-flag","closed-mailbox-with-lowered-flag","open-mailbox-with-raised-flag","open-mailbox-with-lowered-flag","postbox","ballot-box-with-ballot","pencil","black-nib","fountain-pen","pen","paintbrush","crayon","memo","briefcase","file-folder","open-file-folder","card-index-dividers","calendar","tearoff-calendar","spiral-notepad","spiral-calendar","card-index","chart-increasing","chart-decreasing","bar-chart","clipboard","pushpin","round-pushpin","paperclip","linked-paperclips","straight-ruler","triangular-ruler","scissors","card-file-box","file-cabinet","wastebasket","locked","unlocked","locked-with-pen","locked-with-key","key","old-key","hammer","axe","pick","hammer-and-pick","hammer-and-wrench","dagger","crossed-swords","bomb","boomerang","bow-and-arrow","shield","carpentry-saw","wrench","screwdriver","nut-and-bolt","gear","clamp","balance-scale","white-cane","link","broken-chain","chains","hook","toolbox","magnet","ladder","alembic","test-tube","petri-dish","dna","microscope","telescope","satellite-antenna","syringe","drop-of-blood","pill","adhesive-bandage","crutch","stethoscope","xray","door","elevator","mirror","window","bed","couch-and-lamp","chair","toilet","plunger","shower","bathtub","mouse-trap","razor","lotion-bottle","safety-pin","broom","basket","roll-of-paper","bucket","soap","bubbles","toothbrush","sponge","fire-extinguisher","shopping-cart","cigarette","coffin","headstone","funeral-urn","nazar-amulet","hamsa","moai","placard","identification-card"]},{"id":"symbols","name":"Symbols","emojis":["atm-sign","litter-in-bin-sign","potable-water","wheelchair-symbol","mens-room","womens-room","restroom","baby-symbol","water-closet","passport-control","customs","baggage-claim","left-luggage","warning","children-crossing","no-entry","prohibited","no-bicycles","no-smoking","no-littering","nonpotable-water","no-pedestrians","no-mobile-phones","no-one-under-eighteen","radioactive","biohazard","up-arrow","upright-arrow","right-arrow","downright-arrow","down-arrow","downleft-arrow","left-arrow","upleft-arrow","updown-arrow","leftright-arrow","right-arrow-curving-left","left-arrow-curving-right","right-arrow-curving-up","right-arrow-curving-down","clockwise-vertical-arrows","counterclockwise-arrows-button","back-arrow","end-arrow","on-arrow","soon-arrow","top-arrow","place-of-worship","atom-symbol","om","star-of-david","wheel-of-dharma","yin-yang","latin-cross","orthodox-cross","star-and-crescent","peace-symbol","menorah","dotted-sixpointed-star","khanda","aries","taurus","gemini","cancer","leo","virgo","libra","scorpio","sagittarius","capricorn","aquarius","pisces","ophiuchus","shuffle-tracks-button","repeat-button","repeat-single-button","play-button","fastforward-button","next-track-button","play-or-pause-button","reverse-button","fast-reverse-button","last-track-button","upwards-button","fast-up-button","downwards-button","fast-down-button","pause-button","stop-button","record-button","eject-button","cinema","dim-button","bright-button","antenna-bars","wireless","vibration-mode","mobile-phone-off","female-sign","male-sign","transgender-symbol","multiply","plus","minus","divide","heavy-equals-sign","infinity","double-exclamation-mark","exclamation-question-mark","red-question-mark","white-question-mark","white-exclamation-mark","red-exclamation-mark","wavy-dash","currency-exchange","heavy-dollar-sign","medical-symbol","recycling-symbol","fleurdelis","trident-emblem","name-badge","japanese-symbol-for-beginner","hollow-red-circle","check-mark-button","check-box-with-check","check-mark","cross-mark","cross-mark-button","curly-loop","double-curly-loop","part-alternation-mark","eightspoked-asterisk","eightpointed-star","sparkle","copyright","registered","trade-mark","keycap","keycap","keycap-0","keycap-1","keycap-2","keycap-3","keycap-4","keycap-5","keycap-6","keycap-7","keycap-8","keycap-9","keycap-10","input-latin-uppercase","input-latin-lowercase","input-numbers","input-symbols","input-latin-letters","a-button-blood-type","ab-button-blood-type","b-button-blood-type","cl-button","cool-button","free-button","information","id-button","circled-m","new-button","ng-button","o-button-blood-type","ok-button","p-button","sos-button","up-button","vs-button","japanese-here-button","japanese-service-charge-button","japanese-monthly-amount-button","japanese-not-free-of-charge-button","japanese-reserved-button","japanese-bargain-button","japanese-discount-button","japanese-free-of-charge-button","japanese-prohibited-button","japanese-acceptable-button","japanese-application-button","japanese-passing-grade-button","japanese-vacancy-button","japanese-congratulations-button","japanese-secret-button","japanese-open-for-business-button","japanese-no-vacancy-button","red-circle","orange-circle","yellow-circle","green-circle","blue-circle","purple-circle","brown-circle","black-circle","white-circle","red-square","orange-square","yellow-square","green-square","blue-square","purple-square","brown-square","black-large-square","white-large-square","black-medium-square","white-medium-square","black-mediumsmall-square","white-mediumsmall-square","black-small-square","white-small-square","large-orange-diamond","large-blue-diamond","small-orange-diamond","small-blue-diamond","red-triangle-pointed-up","red-triangle-pointed-down","diamond-with-a-dot","radio-button","white-square-button","black-square-button"]},{"id":"flags","name":"Flags","emojis":["chequered-flag","triangular-flag","crossed-flags","black-flag","white-flag","rainbow-flag","transgender-flag","pirate-flag","flag-ascension-island","flag-andorra","flag-united-arab-emirates","flag-afghanistan","flag-antigua--barbuda","flag-anguilla","flag-albania","flag-armenia","flag-angola","flag-antarctica","flag-argentina","flag-american-samoa","flag-austria","flag-australia","flag-aruba","flag-land-islands","flag-azerbaijan","flag-bosnia--herzegovina","flag-barbados","flag-bangladesh","flag-belgium","flag-burkina-faso","flag-bulgaria","flag-bahrain","flag-burundi","flag-benin","flag-st-barthlemy","flag-bermuda","flag-brunei","flag-bolivia","flag-caribbean-netherlands","flag-brazil","flag-bahamas","flag-bhutan","flag-bouvet-island","flag-botswana","flag-belarus","flag-belize","flag-canada","flag-cocos-keeling-islands","flag-congo--kinshasa","flag-central-african-republic","flag-congo--brazzaville","flag-switzerland","flag-cte-divoire","flag-cook-islands","flag-chile","flag-cameroon","flag-china","flag-colombia","flag-clipperton-island","flag-costa-rica","flag-cuba","flag-cape-verde","flag-curaao","flag-christmas-island","flag-cyprus","flag-czechia","flag-germany","flag-diego-garcia","flag-djibouti","flag-denmark","flag-dominica","flag-dominican-republic","flag-algeria","flag-ceuta--melilla","flag-ecuador","flag-estonia","flag-egypt","flag-western-sahara","flag-eritrea","flag-spain","flag-ethiopia","flag-european-union","flag-finland","flag-fiji","flag-falkland-islands","flag-micronesia","flag-faroe-islands","flag-france","flag-gabon","flag-united-kingdom","flag-grenada","flag-georgia","flag-french-guiana","flag-guernsey","flag-ghana","flag-gibraltar","flag-greenland","flag-gambia","flag-guinea","flag-guadeloupe","flag-equatorial-guinea","flag-greece","flag-south-georgia--south-sandwich-islands","flag-guatemala","flag-guam","flag-guineabissau","flag-guyana","flag-hong-kong-sar-china","flag-heard--mcdonald-islands","flag-honduras","flag-croatia","flag-haiti","flag-hungary","flag-canary-islands","flag-indonesia","flag-ireland","flag-israel","flag-isle-of-man","flag-india","flag-british-indian-ocean-territory","flag-iraq","flag-iran","flag-iceland","flag-italy","flag-jersey","flag-jamaica","flag-jordan","flag-japan","flag-kenya","flag-kyrgyzstan","flag-cambodia","flag-kiribati","flag-comoros","flag-st-kitts--nevis","flag-north-korea","flag-south-korea","flag-kuwait","flag-cayman-islands","flag-kazakhstan","flag-laos","flag-lebanon","flag-st-lucia","flag-liechtenstein","flag-sri-lanka","flag-liberia","flag-lesotho","flag-lithuania","flag-luxembourg","flag-latvia","flag-libya","flag-morocco","flag-monaco","flag-moldova","flag-montenegro","flag-st-martin","flag-madagascar","flag-marshall-islands","flag-north-macedonia","flag-mali","flag-myanmar-burma","flag-mongolia","flag-macao-sar-china","flag-northern-mariana-islands","flag-martinique","flag-mauritania","flag-montserrat","flag-malta","flag-mauritius","flag-maldives","flag-malawi","flag-mexico","flag-malaysia","flag-mozambique","flag-namibia","flag-new-caledonia","flag-niger","flag-norfolk-island","flag-nigeria","flag-nicaragua","flag-netherlands","flag-norway","flag-nepal","flag-nauru","flag-niue","flag-new-zealand","flag-oman","flag-panama","flag-peru","flag-french-polynesia","flag-papua-new-guinea","flag-philippines","flag-pakistan","flag-poland","flag-st-pierre--miquelon","flag-pitcairn-islands","flag-puerto-rico","flag-palestinian-territories","flag-portugal","flag-palau","flag-paraguay","flag-qatar","flag-runion","flag-romania","flag-serbia","flag-russia","flag-rwanda","flag-saudi-arabia","flag-solomon-islands","flag-seychelles","flag-sudan","flag-sweden","flag-singapore","flag-st-helena","flag-slovenia","flag-svalbard--jan-mayen","flag-slovakia","flag-sierra-leone","flag-san-marino","flag-senegal","flag-somalia","flag-suriname","flag-south-sudan","flag-so-tom--prncipe","flag-el-salvador","flag-sint-maarten","flag-syria","flag-eswatini","flag-tristan-da-cunha","flag-turks--caicos-islands","flag-chad","flag-french-southern-territories","flag-togo","flag-thailand","flag-tajikistan","flag-tokelau","flag-timorleste","flag-turkmenistan","flag-tunisia","flag-tonga","flag-trkiye","flag-trinidad--tobago","flag-tuvalu","flag-taiwan","flag-tanzania","flag-ukraine","flag-uganda","flag-us-outlying-islands","flag-united-nations","flag-united-states","flag-uruguay","flag-uzbekistan","flag-vatican-city","flag-st-vincent--grenadines","flag-venezuela","flag-british-virgin-islands","flag-us-virgin-islands","flag-vietnam","flag-vanuatu","flag-wallis--futuna","flag-samoa","flag-kosovo","flag-yemen","flag-mayotte","flag-south-africa","flag-zambia","flag-zimbabwe","flag-england","flag-scotland","flag-wales"]}],"emojis":{"grinning-face":{"a":"Grinning Face","b":"1F600","j":["face","grin","smile","happy","joy",":D"]},"grinning-face-with-big-eyes":{"a":"Grinning Face with Big Eyes","b":"1F603","j":["face","mouth","open","smile","happy","joy","haha",":D",":)","funny"]},"grinning-face-with-smiling-eyes":{"a":"Grinning Face with Smiling Eyes","b":"1F604","j":["eye","face","mouth","open","smile","happy","joy","funny","haha","laugh","like",":D",":)"]},"beaming-face-with-smiling-eyes":{"a":"Beaming Face with Smiling Eyes","b":"1F601","j":["eye","face","grin","smile","happy","joy","kawaii"]},"grinning-squinting-face":{"a":"Grinning Squinting Face","b":"1F606","j":["face","laugh","mouth","satisfied","smile","happy","joy","lol","haha","glad","XD"]},"grinning-face-with-sweat":{"a":"Grinning Face with Sweat","b":"1F605","j":["cold","face","open","smile","sweat","hot","happy","laugh","relief"]},"rolling-on-the-floor-laughing":{"a":"Rolling on the Floor Laughing","b":"1F923","j":["face","floor","laugh","rofl","rolling","rotfl","laughing","lol","haha"]},"face-with-tears-of-joy":{"a":"Face with Tears of Joy","b":"1F602","j":["face","joy","laugh","tear","cry","tears","weep","happy","happytears","haha"]},"slightly-smiling-face":{"a":"Slightly Smiling Face","b":"1F642","j":["face","smile"]},"upsidedown-face":{"a":"Upside-Down Face","b":"1F643","j":["face","upside-down","upside_down_face","flipped","silly","smile"]},"melting-face":{"a":"Melting Face","b":"1FAE0","j":["disappear","dissolve","liquid","melt","hot","heat"]},"winking-face":{"a":"Winking Face","b":"1F609","j":["face","wink","happy","mischievous","secret",";)","smile","eye"]},"smiling-face-with-smiling-eyes":{"a":"Smiling Face with Smiling Eyes","b":"1F60A","j":["blush","eye","face","smile","happy","flushed","crush","embarrassed","shy","joy"]},"smiling-face-with-halo":{"a":"Smiling Face with Halo","b":"1F607","j":["angel","face","fantasy","halo","innocent","heaven"]},"smiling-face-with-hearts":{"a":"Smiling Face with Hearts","b":"1F970","j":["adore","crush","hearts","in love","face","love","like","affection","valentines","infatuation"]},"smiling-face-with-hearteyes":{"a":"Smiling Face with Heart-Eyes","b":"1F60D","j":["eye","face","love","smile","smiling face with heart-eyes","smiling_face_with_heart_eyes","like","affection","valentines","infatuation","crush","heart"]},"starstruck":{"a":"Star-Struck","b":"1F929","j":["eyes","face","grinning","star","star-struck","starry-eyed","star_struck","smile","starry"]},"face-blowing-a-kiss":{"a":"Face Blowing a Kiss","b":"1F618","j":["face","kiss","love","like","affection","valentines","infatuation"]},"kissing-face":{"a":"Kissing Face","b":"1F617","j":["face","kiss","love","like","3","valentines","infatuation"]},"smiling-face":{"a":"Smiling Face","b":"263A-FE0F","j":["face","outlined","relaxed","smile","blush","massage","happiness"]},"kissing-face-with-closed-eyes":{"a":"Kissing Face with Closed Eyes","b":"1F61A","j":["closed","eye","face","kiss","love","like","affection","valentines","infatuation"]},"kissing-face-with-smiling-eyes":{"a":"Kissing Face with Smiling Eyes","b":"1F619","j":["eye","face","kiss","smile","affection","valentines","infatuation"]},"smiling-face-with-tear":{"a":"Smiling Face with Tear","b":"1F972","j":["grateful","proud","relieved","smiling","tear","touched","sad","cry","pretend"]},"face-savoring-food":{"a":"Face Savoring Food","b":"1F60B","j":["delicious","face","savouring","smile","yum","happy","joy","tongue","silly","yummy","nom"]},"face-with-tongue":{"a":"Face with Tongue","b":"1F61B","j":["face","tongue","prank","childish","playful","mischievous","smile"]},"winking-face-with-tongue":{"a":"Winking Face with Tongue","b":"1F61C","j":["eye","face","joke","tongue","wink","prank","childish","playful","mischievous","smile"]},"zany-face":{"a":"Zany Face","b":"1F92A","j":["eye","goofy","large","small","face","crazy"]},"squinting-face-with-tongue":{"a":"Squinting Face with Tongue","b":"1F61D","j":["eye","face","horrible","taste","tongue","prank","playful","mischievous","smile"]},"moneymouth-face":{"a":"Money-Mouth Face","b":"1F911","j":["face","money","money-mouth face","mouth","money_mouth_face","rich","dollar"]},"smiling-face-with-open-hands":{"a":"Smiling Face with Open Hands","b":"1F917","j":["face","hug","hugging","open hands","smiling face","hugging_face","smile"]},"face-with-hand-over-mouth":{"a":"Face with Hand over Mouth","b":"1F92D","j":["whoops","shock","sudden realization","surprise","face"]},"face-with-open-eyes-and-hand-over-mouth":{"a":"Face with Open Eyes and Hand over Mouth","b":"1FAE2","j":["amazement","awe","disbelief","embarrass","scared","surprise","silence","secret","shock"]},"face-with-peeking-eye":{"a":"Face with Peeking Eye","b":"1FAE3","j":["captivated","peep","stare","scared","frightening","embarrassing","shy"]},"shushing-face":{"a":"Shushing Face","b":"1F92B","j":["quiet","shush","face","shhh"]},"thinking-face":{"a":"Thinking Face","b":"1F914","j":["face","thinking","hmmm","think","consider"]},"saluting-face":{"a":"Saluting Face","b":"1FAE1","j":["OK","salute","sunny","troops","yes","respect"]},"zippermouth-face":{"a":"Zipper-Mouth Face","b":"1F910","j":["face","mouth","zip","zipper","zipper-mouth face","zipper_mouth_face","sealed","secret"]},"face-with-raised-eyebrow":{"a":"Face with Raised Eyebrow","b":"1F928","j":["distrust","skeptic","disapproval","disbelief","mild surprise","scepticism","face","surprise","suspicious"]},"neutral-face":{"a":"Neutral Face","b":"1F610-FE0F","j":["deadpan","face","meh","neutral","indifference",":|"]},"expressionless-face":{"a":"Expressionless Face","b":"1F611","j":["expressionless","face","inexpressive","meh","unexpressive","indifferent","-_-","deadpan"]},"face-without-mouth":{"a":"Face Without Mouth","b":"1F636","j":["face","mouth","quiet","silent"]},"dotted-line-face":{"a":"Dotted Line Face","b":"1FAE5","j":["depressed","disappear","hide","introvert","invisible","lonely","isolation","depression"]},"face-in-clouds":{"a":"Face in Clouds","b":"1F636-200D-1F32B-FE0F","j":["absentminded","face in the fog","head in clouds","shower","steam","dream"]},"smirking-face":{"a":"Smirking Face","b":"1F60F","j":["face","smirk","smile","mean","prank","smug","sarcasm"]},"unamused-face":{"a":"Unamused Face","b":"1F612","j":["face","unamused","unhappy","indifference","bored","straight face","serious","sarcasm","unimpressed","skeptical","dubious","ugh","side_eye"]},"face-with-rolling-eyes":{"a":"Face with Rolling Eyes","b":"1F644","j":["eyeroll","eyes","face","rolling","frustrated"]},"grimacing-face":{"a":"Grimacing Face","b":"1F62C","j":["face","grimace","teeth"]},"face-exhaling":{"a":"Face Exhaling","b":"1F62E-200D-1F4A8","j":["exhale","gasp","groan","relief","whisper","whistle","relieve","tired","sigh"]},"lying-face":{"a":"Lying Face","b":"1F925","j":["face","lie","pinocchio"]},"shaking-face":{"a":"Shaking Face","b":"1FAE8","j":["earthquake","face","shaking","shock","vibrate","dizzy","blurry"]},"head-shaking-horizontally":{"a":"⊛ Head Shaking Horizontally","b":"1F642-200D-2194-FE0F","j":["head shaking horizontally","no","shake"]},"head-shaking-vertically":{"a":"⊛ Head Shaking Vertically","b":"1F642-200D-2195-FE0F","j":["head shaking vertically","nod","yes"]},"relieved-face":{"a":"Relieved Face","b":"1F60C","j":["face","relieved","relaxed","phew","massage","happiness"]},"pensive-face":{"a":"Pensive Face","b":"1F614","j":["dejected","face","pensive","sad","depressed","upset"]},"sleepy-face":{"a":"Sleepy Face","b":"1F62A","j":["face","good night","sleep","tired","rest","nap"]},"drooling-face":{"a":"Drooling Face","b":"1F924","j":["drooling","face"]},"sleeping-face":{"a":"Sleeping Face","b":"1F634","j":["face","good night","sleep","ZZZ","tired","sleepy","night","zzz"]},"face-with-medical-mask":{"a":"Face with Medical Mask","b":"1F637","j":["cold","doctor","face","mask","sick","ill","disease","covid"]},"face-with-thermometer":{"a":"Face with Thermometer","b":"1F912","j":["face","ill","sick","thermometer","temperature","cold","fever","covid"]},"face-with-headbandage":{"a":"Face with Head-Bandage","b":"1F915","j":["bandage","face","face with head-bandage","hurt","injury","face_with_head_bandage","injured","clumsy"]},"nauseated-face":{"a":"Nauseated Face","b":"1F922","j":["face","nauseated","vomit","gross","green","sick","throw up","ill"]},"face-vomiting":{"a":"Face Vomiting","b":"1F92E","j":["puke","sick","vomit","face"]},"sneezing-face":{"a":"Sneezing Face","b":"1F927","j":["face","gesundheit","sneeze","sick","allergy"]},"hot-face":{"a":"Hot Face","b":"1F975","j":["feverish","heat stroke","hot","red-faced","sweating","face","heat","red"]},"cold-face":{"a":"Cold Face","b":"1F976","j":["blue-faced","cold","freezing","frostbite","icicles","face","blue","frozen"]},"woozy-face":{"a":"Woozy Face","b":"1F974","j":["dizzy","intoxicated","tipsy","uneven eyes","wavy mouth","face","wavy"]},"face-with-crossedout-eyes":{"a":"Face with Crossed-out Eyes","b":"1F635","j":["crossed-out eyes","dead","face","face with crossed-out eyes","knocked out","dizzy_face","spent","unconscious","xox","dizzy"]},"face-with-spiral-eyes":{"a":"Face with Spiral Eyes","b":"1F635-200D-1F4AB","j":["dizzy","hypnotized","spiral","trouble","whoa","sick","ill","confused","nauseous","nausea"]},"exploding-head":{"a":"Exploding Head","b":"1F92F","j":["mind blown","shocked","face","mind","blown"]},"cowboy-hat-face":{"a":"Cowboy Hat Face","b":"1F920","j":["cowboy","cowgirl","face","hat"]},"partying-face":{"a":"Partying Face","b":"1F973","j":["celebration","hat","horn","party","face","woohoo"]},"disguised-face":{"a":"Disguised Face","b":"1F978","j":["disguise","face","glasses","incognito","nose","pretent","brows","moustache"]},"smiling-face-with-sunglasses":{"a":"Smiling Face with Sunglasses","b":"1F60E","j":["bright","cool","face","sun","sunglasses","smile","summer","beach","sunglass"]},"nerd-face":{"a":"Nerd Face","b":"1F913","j":["face","geek","nerd","nerdy","dork"]},"face-with-monocle":{"a":"Face with Monocle","b":"1F9D0","j":["face","monocle","stuffy","wealthy"]},"confused-face":{"a":"Confused Face","b":"1F615","j":["confused","face","meh","indifference","huh","weird","hmmm",":/"]},"face-with-diagonal-mouth":{"a":"Face with Diagonal Mouth","b":"1FAE4","j":["disappointed","meh","skeptical","unsure","skeptic","confuse","frustrated","indifferent"]},"worried-face":{"a":"Worried Face","b":"1F61F","j":["face","worried","concern","nervous",":("]},"slightly-frowning-face":{"a":"Slightly Frowning Face","b":"1F641","j":["face","frown","frowning","disappointed","sad","upset"]},"frowning-face":{"a":"Frowning Face","b":"2639-FE0F","j":["face","frown","sad","upset"]},"face-with-open-mouth":{"a":"Face with Open Mouth","b":"1F62E","j":["face","mouth","open","sympathy","surprise","impressed","wow","whoa",":O"]},"hushed-face":{"a":"Hushed Face","b":"1F62F","j":["face","hushed","stunned","surprised","woo","shh"]},"astonished-face":{"a":"Astonished Face","b":"1F632","j":["astonished","face","shocked","totally","xox","surprised","poisoned"]},"flushed-face":{"a":"Flushed Face","b":"1F633","j":["dazed","face","flushed","blush","shy","flattered"]},"pleading-face":{"a":"Pleading Face","b":"1F97A","j":["begging","mercy","puppy eyes","face","cry","tears","sad","grievance"]},"face-holding-back-tears":{"a":"Face Holding Back Tears","b":"1F979","j":["angry","cry","proud","resist","sad","touched","gratitude"]},"frowning-face-with-open-mouth":{"a":"Frowning Face with Open Mouth","b":"1F626","j":["face","frown","mouth","open","aw","what"]},"anguished-face":{"a":"Anguished Face","b":"1F627","j":["anguished","face","stunned","nervous"]},"fearful-face":{"a":"Fearful Face","b":"1F628","j":["face","fear","fearful","scared","terrified","nervous"]},"anxious-face-with-sweat":{"a":"Anxious Face with Sweat","b":"1F630","j":["blue","cold","face","rushed","sweat","nervous"]},"sad-but-relieved-face":{"a":"Sad but Relieved Face","b":"1F625","j":["disappointed","face","relieved","whew","phew","sweat","nervous"]},"crying-face":{"a":"Crying Face","b":"1F622","j":["cry","face","sad","tear","tears","depressed","upset",":'("]},"loudly-crying-face":{"a":"Loudly Crying Face","b":"1F62D","j":["cry","face","sad","sob","tear","sobbing","tears","upset","depressed"]},"face-screaming-in-fear":{"a":"Face Screaming in Fear","b":"1F631","j":["face","fear","munch","scared","scream","omg"]},"confounded-face":{"a":"Confounded Face","b":"1F616","j":["confounded","face","confused","sick","unwell","oops",":S"]},"persevering-face":{"a":"Persevering Face","b":"1F623","j":["face","persevere","sick","no","upset","oops"]},"disappointed-face":{"a":"Disappointed Face","b":"1F61E","j":["disappointed","face","sad","upset","depressed",":("]},"downcast-face-with-sweat":{"a":"Downcast Face with Sweat","b":"1F613","j":["cold","face","sweat","hot","sad","tired","exercise"]},"weary-face":{"a":"Weary Face","b":"1F629","j":["face","tired","weary","sleepy","sad","frustrated","upset"]},"tired-face":{"a":"Tired Face","b":"1F62B","j":["face","tired","sick","whine","upset","frustrated"]},"yawning-face":{"a":"Yawning Face","b":"1F971","j":["bored","tired","yawn","sleepy"]},"face-with-steam-from-nose":{"a":"Face with Steam From Nose","b":"1F624","j":["face","triumph","won","gas","phew","proud","pride"]},"enraged-face":{"a":"Enraged Face","b":"1F621","j":["angry","enraged","face","mad","pouting","rage","red","pouting_face","hate","despise"]},"angry-face":{"a":"Angry Face","b":"1F620","j":["anger","angry","face","mad","annoyed","frustrated"]},"face-with-symbols-on-mouth":{"a":"Face with Symbols on Mouth","b":"1F92C","j":["swearing","cursing","face","cussing","profanity","expletive"]},"smiling-face-with-horns":{"a":"Smiling Face with Horns","b":"1F608","j":["face","fairy tale","fantasy","horns","smile","devil"]},"angry-face-with-horns":{"a":"Angry Face with Horns","b":"1F47F","j":["demon","devil","face","fantasy","imp","angry","horns"]},"skull":{"a":"Skull","b":"1F480","j":["death","face","fairy tale","monster","dead","skeleton","creepy"]},"skull-and-crossbones":{"a":"Skull and Crossbones","b":"2620-FE0F","j":["crossbones","death","face","monster","skull","poison","danger","deadly","scary","pirate","evil"]},"pile-of-poo":{"a":"Pile of Poo","b":"1F4A9","j":["dung","face","monster","poo","poop","hankey","shitface","fail","turd","shit"]},"clown-face":{"a":"Clown Face","b":"1F921","j":["clown","face"]},"ogre":{"a":"Ogre","b":"1F479","j":["creature","face","fairy tale","fantasy","monster","troll","red","mask","halloween","scary","creepy","devil","demon","japanese_ogre"]},"goblin":{"a":"Goblin","b":"1F47A","j":["creature","face","fairy tale","fantasy","monster","red","evil","mask","scary","creepy","japanese_goblin"]},"ghost":{"a":"Ghost","b":"1F47B","j":["creature","face","fairy tale","fantasy","monster","halloween","spooky","scary"]},"alien":{"a":"Alien","b":"1F47D-FE0F","j":["creature","extraterrestrial","face","fantasy","ufo","UFO","paul","weird","outer_space"]},"alien-monster":{"a":"Alien Monster","b":"1F47E","j":["alien","creature","extraterrestrial","face","monster","ufo","game","arcade","play"]},"robot":{"a":"Robot","b":"1F916","j":["face","monster","computer","machine","bot"]},"grinning-cat":{"a":"Grinning Cat","b":"1F63A","j":["cat","face","grinning","mouth","open","smile","animal","cats","happy"]},"grinning-cat-with-smiling-eyes":{"a":"Grinning Cat with Smiling Eyes","b":"1F638","j":["cat","eye","face","grin","smile","animal","cats"]},"cat-with-tears-of-joy":{"a":"Cat with Tears of Joy","b":"1F639","j":["cat","face","joy","tear","animal","cats","haha","happy","tears"]},"smiling-cat-with-hearteyes":{"a":"Smiling Cat with Heart-Eyes","b":"1F63B","j":["cat","eye","face","heart","love","smile","smiling cat with heart-eyes","smiling_cat_with_heart_eyes","animal","like","affection","cats","valentines"]},"cat-with-wry-smile":{"a":"Cat with Wry Smile","b":"1F63C","j":["cat","face","ironic","smile","wry","animal","cats","smirk"]},"kissing-cat":{"a":"Kissing Cat","b":"1F63D","j":["cat","eye","face","kiss","animal","cats"]},"weary-cat":{"a":"Weary Cat","b":"1F640","j":["cat","face","oh","surprised","weary","animal","cats","munch","scared","scream"]},"crying-cat":{"a":"Crying Cat","b":"1F63F","j":["cat","cry","face","sad","tear","animal","tears","weep","cats","upset"]},"pouting-cat":{"a":"Pouting Cat","b":"1F63E","j":["cat","face","pouting","animal","cats"]},"seenoevil-monkey":{"a":"See-No-Evil Monkey","b":"1F648","j":["evil","face","forbidden","monkey","see","see-no-evil monkey","see_no_evil_monkey","animal","nature","haha"]},"hearnoevil-monkey":{"a":"Hear-No-Evil Monkey","b":"1F649","j":["evil","face","forbidden","hear","hear-no-evil monkey","monkey","hear_no_evil_monkey","animal","nature"]},"speaknoevil-monkey":{"a":"Speak-No-Evil Monkey","b":"1F64A","j":["evil","face","forbidden","monkey","speak","speak-no-evil monkey","speak_no_evil_monkey","animal","nature","omg"]},"love-letter":{"a":"Love Letter","b":"1F48C","j":["heart","letter","love","mail","email","like","affection","envelope","valentines"]},"heart-with-arrow":{"a":"Heart with Arrow","b":"1F498","j":["arrow","cupid","love","like","heart","affection","valentines"]},"heart-with-ribbon":{"a":"Heart with Ribbon","b":"1F49D","j":["ribbon","valentine","love","valentines"]},"sparkling-heart":{"a":"Sparkling Heart","b":"1F496","j":["excited","sparkle","love","like","affection","valentines"]},"growing-heart":{"a":"Growing Heart","b":"1F497","j":["excited","growing","nervous","pulse","like","love","affection","valentines","pink"]},"beating-heart":{"a":"Beating Heart","b":"1F493","j":["beating","heartbeat","pulsating","love","like","affection","valentines","pink","heart"]},"revolving-hearts":{"a":"Revolving Hearts","b":"1F49E","j":["revolving","love","like","affection","valentines"]},"two-hearts":{"a":"Two Hearts","b":"1F495","j":["love","like","affection","valentines","heart"]},"heart-decoration":{"a":"Heart Decoration","b":"1F49F","j":["heart","purple-square","love","like"]},"heart-exclamation":{"a":"Heart Exclamation","b":"2763-FE0F","j":["exclamation","mark","punctuation","decoration","love"]},"broken-heart":{"a":"Broken Heart","b":"1F494","j":["break","broken","sad","sorry","heart","heartbreak"]},"heart-on-fire":{"a":"Heart on Fire","b":"2764-FE0F-200D-1F525","j":["burn","heart","love","lust","sacred heart","passionate","enthusiastic"]},"mending-heart":{"a":"Mending Heart","b":"2764-FE0F-200D-1FA79","j":["healthier","improving","mending","recovering","recuperating","well","broken heart","bandage","wounded"]},"red-heart":{"a":"Red Heart","b":"2764-FE0F","j":["heart","love","like","valentines"]},"pink-heart":{"a":"Pink Heart","b":"1FA77","j":["cute","heart","like","love","pink","valentines"]},"orange-heart":{"a":"Orange Heart","b":"1F9E1","j":["orange","love","like","affection","valentines"]},"yellow-heart":{"a":"Yellow Heart","b":"1F49B","j":["yellow","love","like","affection","valentines"]},"green-heart":{"a":"Green Heart","b":"1F49A","j":["green","love","like","affection","valentines"]},"blue-heart":{"a":"Blue Heart","b":"1F499","j":["blue","love","like","affection","valentines"]},"light-blue-heart":{"a":"Light Blue Heart","b":"1FA75","j":["cyan","heart","light blue","teal","ice","baby blue"]},"purple-heart":{"a":"Purple Heart","b":"1F49C","j":["purple","love","like","affection","valentines"]},"brown-heart":{"a":"Brown Heart","b":"1F90E","j":["brown","heart","coffee"]},"black-heart":{"a":"Black Heart","b":"1F5A4","j":["black","evil","wicked"]},"grey-heart":{"a":"Grey Heart","b":"1FA76","j":["gray","heart","silver","slate","monochrome"]},"white-heart":{"a":"White Heart","b":"1F90D","j":["heart","white","pure"]},"kiss-mark":{"a":"Kiss Mark","b":"1F48B","j":["kiss","lips","face","love","like","affection","valentines"]},"hundred-points":{"a":"Hundred Points","b":"1F4AF","j":["100","full","hundred","score","perfect","numbers","century","exam","quiz","test","pass"]},"anger-symbol":{"a":"Anger Symbol","b":"1F4A2","j":["angry","comic","mad"]},"collision":{"a":"Collision","b":"1F4A5","j":["boom","comic","bomb","explode","explosion","blown"]},"dizzy":{"a":"Dizzy","b":"1F4AB","j":["comic","star","sparkle","shoot","magic"]},"sweat-droplets":{"a":"Sweat Droplets","b":"1F4A6","j":["comic","splashing","sweat","water","drip","oops"]},"dashing-away":{"a":"Dashing Away","b":"1F4A8","j":["comic","dash","running","wind","air","fast","shoo","fart","smoke","puff"]},"hole":{"a":"Hole","b":"1F573-FE0F","j":["embarrassing"]},"speech-balloon":{"a":"Speech Balloon","b":"1F4AC","j":["balloon","bubble","comic","dialog","speech","words","message","talk","chatting"]},"eye-in-speech-bubble":{"a":"Eye in Speech Bubble","b":"1F441-FE0F-200D-1F5E8-FE0F","j":["balloon","bubble","eye","speech","witness","info"]},"left-speech-bubble":{"a":"Left Speech Bubble","b":"1F5E8-FE0F","j":["balloon","bubble","dialog","speech","words","message","talk","chatting"]},"right-anger-bubble":{"a":"Right Anger Bubble","b":"1F5EF-FE0F","j":["angry","balloon","bubble","mad","caption","speech","thinking"]},"thought-balloon":{"a":"Thought Balloon","b":"1F4AD","j":["balloon","bubble","comic","thought","cloud","speech","thinking","dream"]},"zzz":{"a":"Zzz","b":"1F4A4","j":["comic","good night","sleep","ZZZ","sleepy","tired","dream"]},"waving-hand":{"a":"Waving Hand","b":"1F44B","j":["hand","wave","waving","hands","gesture","goodbye","solong","farewell","hello","hi","palm"]},"raised-back-of-hand":{"a":"Raised Back of Hand","b":"1F91A","j":["backhand","raised","fingers"]},"hand-with-fingers-splayed":{"a":"Hand with Fingers Splayed","b":"1F590-FE0F","j":["finger","hand","splayed","fingers","palm"]},"raised-hand":{"a":"Raised Hand","b":"270B","j":["hand","high 5","high five","fingers","stop","highfive","palm","ban"]},"vulcan-salute":{"a":"Vulcan Salute","b":"1F596","j":["finger","hand","spock","vulcan","fingers","star trek"]},"rightwards-hand":{"a":"Rightwards Hand","b":"1FAF1","j":["hand","right","rightward","palm","offer"]},"leftwards-hand":{"a":"Leftwards Hand","b":"1FAF2","j":["hand","left","leftward","palm","offer"]},"palm-down-hand":{"a":"Palm Down Hand","b":"1FAF3","j":["dismiss","drop","shoo","palm"]},"palm-up-hand":{"a":"Palm Up Hand","b":"1FAF4","j":["beckon","catch","come","offer","lift","demand"]},"leftwards-pushing-hand":{"a":"Leftwards Pushing Hand","b":"1FAF7","j":["high five","leftward","push","refuse","stop","wait","highfive","pressing"]},"rightwards-pushing-hand":{"a":"Rightwards Pushing Hand","b":"1FAF8","j":["high five","push","refuse","rightward","stop","wait","highfive","pressing"]},"ok-hand":{"a":"Ok Hand","b":"1F44C","j":["hand","OK","fingers","limbs","perfect","ok","okay"]},"pinched-fingers":{"a":"Pinched Fingers","b":"1F90C","j":["fingers","hand gesture","interrogation","pinched","sarcastic","size","tiny","small"]},"pinching-hand":{"a":"Pinching Hand","b":"1F90F","j":["small amount","tiny","small","size"]},"victory-hand":{"a":"Victory Hand","b":"270C-FE0F","j":["hand","v","victory","fingers","ohyeah","peace","two"]},"crossed-fingers":{"a":"Crossed Fingers","b":"1F91E","j":["cross","finger","hand","luck","good","lucky"]},"hand-with-index-finger-and-thumb-crossed":{"a":"Hand with Index Finger and Thumb Crossed","b":"1FAF0","j":["expensive","heart","love","money","snap"]},"loveyou-gesture":{"a":"Love-You Gesture","b":"1F91F","j":["hand","ILY","love-you gesture","love_you_gesture","fingers","gesture"]},"sign-of-the-horns":{"a":"Sign of the Horns","b":"1F918","j":["finger","hand","horns","rock-on","fingers","evil_eye","sign_of_horns","rock_on"]},"call-me-hand":{"a":"Call Me Hand","b":"1F919","j":["call","hand","hang loose","Shaka","hands","gesture","shaka"]},"backhand-index-pointing-left":{"a":"Backhand Index Pointing Left","b":"1F448-FE0F","j":["backhand","finger","hand","index","point","direction","fingers","left"]},"backhand-index-pointing-right":{"a":"Backhand Index Pointing Right","b":"1F449-FE0F","j":["backhand","finger","hand","index","point","fingers","direction","right"]},"backhand-index-pointing-up":{"a":"Backhand Index Pointing Up","b":"1F446-FE0F","j":["backhand","finger","hand","point","up","fingers","direction"]},"middle-finger":{"a":"Middle Finger","b":"1F595","j":["finger","hand","fingers","rude","middle","flipping"]},"backhand-index-pointing-down":{"a":"Backhand Index Pointing Down","b":"1F447-FE0F","j":["backhand","down","finger","hand","point","fingers","direction"]},"index-pointing-up":{"a":"Index Pointing Up","b":"261D-FE0F","j":["finger","hand","index","point","up","fingers","direction"]},"index-pointing-at-the-viewer":{"a":"Index Pointing at the Viewer","b":"1FAF5","j":["point","you","recruit"]},"thumbs-up":{"a":"Thumbs Up","b":"1F44D-FE0F","j":["+1","hand","thumb","up","thumbsup","yes","awesome","good","agree","accept","cool","like"]},"thumbs-down":{"a":"Thumbs Down","b":"1F44E-FE0F","j":["-1","down","hand","thumb","thumbsdown","no","dislike"]},"raised-fist":{"a":"Raised Fist","b":"270A","j":["clenched","fist","hand","punch","fingers","grasp"]},"oncoming-fist":{"a":"Oncoming Fist","b":"1F44A","j":["clenched","fist","hand","punch","angry","violence","hit","attack"]},"leftfacing-fist":{"a":"Left-Facing Fist","b":"1F91B","j":["fist","left-facing fist","leftwards","left_facing_fist","hand","fistbump"]},"rightfacing-fist":{"a":"Right-Facing Fist","b":"1F91C","j":["fist","right-facing fist","rightwards","right_facing_fist","hand","fistbump"]},"clapping-hands":{"a":"Clapping Hands","b":"1F44F","j":["clap","hand","hands","praise","applause","congrats","yay"]},"raising-hands":{"a":"Raising Hands","b":"1F64C","j":["celebration","gesture","hand","hooray","raised","yea","hands"]},"heart-hands":{"a":"Heart Hands","b":"1FAF6","j":["love","appreciation","support"]},"open-hands":{"a":"Open Hands","b":"1F450","j":["hand","open","fingers","butterfly","hands"]},"palms-up-together":{"a":"Palms Up Together","b":"1F932","j":["prayer","cupped hands","hands","gesture","cupped"]},"handshake":{"a":"Handshake","b":"1F91D","j":["agreement","hand","meeting","shake"]},"folded-hands":{"a":"Folded Hands","b":"1F64F","j":["ask","hand","high 5","high five","please","pray","thanks","hope","wish","namaste","highfive","thank you","appreciate"]},"writing-hand":{"a":"Writing Hand","b":"270D-FE0F","j":["hand","write","lower_left_ballpoint_pen","stationery","compose"]},"nail-polish":{"a":"Nail Polish","b":"1F485","j":["care","cosmetics","manicure","nail","polish","nail_care","beauty","finger","fashion","slay"]},"selfie":{"a":"Selfie","b":"1F933","j":["camera","phone"]},"flexed-biceps":{"a":"Flexed Biceps","b":"1F4AA","j":["biceps","comic","flex","muscle","arm","hand","summer","strong"]},"mechanical-arm":{"a":"Mechanical Arm","b":"1F9BE","j":["accessibility","prosthetic"]},"mechanical-leg":{"a":"Mechanical Leg","b":"1F9BF","j":["accessibility","prosthetic"]},"leg":{"a":"Leg","b":"1F9B5","j":["kick","limb"]},"foot":{"a":"Foot","b":"1F9B6","j":["kick","stomp"]},"ear":{"a":"Ear","b":"1F442-FE0F","j":["body","face","hear","sound","listen"]},"ear-with-hearing-aid":{"a":"Ear with Hearing Aid","b":"1F9BB","j":["accessibility","hard of hearing"]},"nose":{"a":"Nose","b":"1F443","j":["body","smell","sniff"]},"brain":{"a":"Brain","b":"1F9E0","j":["intelligent","smart"]},"anatomical-heart":{"a":"Anatomical Heart","b":"1FAC0","j":["anatomical","cardiology","heart","organ","pulse","health","heartbeat"]},"lungs":{"a":"Lungs","b":"1FAC1","j":["breath","exhalation","inhalation","organ","respiration","breathe"]},"tooth":{"a":"Tooth","b":"1F9B7","j":["dentist","teeth"]},"bone":{"a":"Bone","b":"1F9B4","j":["skeleton"]},"eyes":{"a":"Eyes","b":"1F440","j":["eye","face","look","watch","stalk","peek","see"]},"eye":{"a":"Eye","b":"1F441-FE0F","j":["body","face","look","see","watch","stare"]},"tongue":{"a":"Tongue","b":"1F445","j":["body","mouth","playful"]},"mouth":{"a":"Mouth","b":"1F444","j":["lips","kiss"]},"biting-lip":{"a":"Biting Lip","b":"1FAE6","j":["anxious","fear","flirting","nervous","uncomfortable","worried","flirt","sexy","pain","worry"]},"baby":{"a":"Baby","b":"1F476","j":["young","child","boy","girl","toddler"]},"child":{"a":"Child","b":"1F9D2","j":["gender-neutral","unspecified gender","young"]},"boy":{"a":"Boy","b":"1F466","j":["young","man","male","guy","teenager"]},"girl":{"a":"Girl","b":"1F467","j":["Virgo","young","zodiac","female","woman","teenager"]},"person":{"a":"Person","b":"1F9D1","j":["adult","gender-neutral","unspecified gender"]},"person-blond-hair":{"a":"Person: Blond Hair","b":"1F471","j":["blond","blond-haired person","hair","person: blond hair","hairstyle"]},"man":{"a":"Man","b":"1F468","j":["adult","mustache","father","dad","guy","classy","sir","moustache"]},"person-beard":{"a":"Person: Beard","b":"1F9D4","j":["beard","person","person: beard","bewhiskered","man_beard"]},"man-beard":{"a":"Man: Beard","b":"1F9D4-200D-2642-FE0F","j":["beard","man","man: beard","facial hair"]},"woman-beard":{"a":"Woman: Beard","b":"1F9D4-200D-2640-FE0F","j":["beard","woman","woman: beard","facial hair"]},"man-red-hair":{"a":"Man: Red Hair","b":"1F468-200D-1F9B0","j":["adult","man","red hair","hairstyle"]},"man-curly-hair":{"a":"Man: Curly Hair","b":"1F468-200D-1F9B1","j":["adult","curly hair","man","hairstyle"]},"man-white-hair":{"a":"Man: White Hair","b":"1F468-200D-1F9B3","j":["adult","man","white hair","old","elder"]},"man-bald":{"a":"Man: Bald","b":"1F468-200D-1F9B2","j":["adult","bald","man","hairless"]},"woman":{"a":"Woman","b":"1F469","j":["adult","female","girls","lady"]},"woman-red-hair":{"a":"Woman: Red Hair","b":"1F469-200D-1F9B0","j":["adult","red hair","woman","hairstyle"]},"person-red-hair":{"a":"Person: Red Hair","b":"1F9D1-200D-1F9B0","j":["adult","gender-neutral","person","red hair","unspecified gender","hairstyle"]},"woman-curly-hair":{"a":"Woman: Curly Hair","b":"1F469-200D-1F9B1","j":["adult","curly hair","woman","hairstyle"]},"person-curly-hair":{"a":"Person: Curly Hair","b":"1F9D1-200D-1F9B1","j":["adult","curly hair","gender-neutral","person","unspecified gender","hairstyle"]},"woman-white-hair":{"a":"Woman: White Hair","b":"1F469-200D-1F9B3","j":["adult","white hair","woman","old","elder"]},"person-white-hair":{"a":"Person: White Hair","b":"1F9D1-200D-1F9B3","j":["adult","gender-neutral","person","unspecified gender","white hair","elder","old"]},"woman-bald":{"a":"Woman: Bald","b":"1F469-200D-1F9B2","j":["adult","bald","woman","hairless"]},"person-bald":{"a":"Person: Bald","b":"1F9D1-200D-1F9B2","j":["adult","bald","gender-neutral","person","unspecified gender","hairless"]},"woman-blond-hair":{"a":"Woman: Blond Hair","b":"1F471-200D-2640-FE0F","j":["blond-haired woman","blonde","hair","woman","woman: blond hair","female","girl","person"]},"man-blond-hair":{"a":"Man: Blond Hair","b":"1F471-200D-2642-FE0F","j":["blond","blond-haired man","hair","man","man: blond hair","male","boy","blonde","guy","person"]},"older-person":{"a":"Older Person","b":"1F9D3","j":["adult","gender-neutral","old","unspecified gender","human","elder","senior"]},"old-man":{"a":"Old Man","b":"1F474","j":["adult","man","old","human","male","men","elder","senior"]},"old-woman":{"a":"Old Woman","b":"1F475","j":["adult","old","woman","human","female","women","lady","elder","senior"]},"person-frowning":{"a":"Person Frowning","b":"1F64D","j":["frown","gesture","worried"]},"man-frowning":{"a":"Man Frowning","b":"1F64D-200D-2642-FE0F","j":["frowning","gesture","man","male","boy","sad","depressed","discouraged","unhappy"]},"woman-frowning":{"a":"Woman Frowning","b":"1F64D-200D-2640-FE0F","j":["frowning","gesture","woman","female","girl","sad","depressed","discouraged","unhappy"]},"person-pouting":{"a":"Person Pouting","b":"1F64E","j":["gesture","pouting","upset"]},"man-pouting":{"a":"Man Pouting","b":"1F64E-200D-2642-FE0F","j":["gesture","man","pouting","male","boy"]},"woman-pouting":{"a":"Woman Pouting","b":"1F64E-200D-2640-FE0F","j":["gesture","pouting","woman","female","girl"]},"person-gesturing-no":{"a":"Person Gesturing No","b":"1F645","j":["forbidden","gesture","hand","person gesturing NO","prohibited","decline"]},"man-gesturing-no":{"a":"Man Gesturing No","b":"1F645-200D-2642-FE0F","j":["forbidden","gesture","hand","man","man gesturing NO","prohibited","male","boy","nope"]},"woman-gesturing-no":{"a":"Woman Gesturing No","b":"1F645-200D-2640-FE0F","j":["forbidden","gesture","hand","prohibited","woman","woman gesturing NO","female","girl","nope"]},"person-gesturing-ok":{"a":"Person Gesturing Ok","b":"1F646","j":["gesture","hand","OK","person gesturing OK","agree"]},"man-gesturing-ok":{"a":"Man Gesturing Ok","b":"1F646-200D-2642-FE0F","j":["gesture","hand","man","man gesturing OK","OK","men","boy","male","blue","human"]},"woman-gesturing-ok":{"a":"Woman Gesturing Ok","b":"1F646-200D-2640-FE0F","j":["gesture","hand","OK","woman","woman gesturing OK","women","girl","female","pink","human"]},"person-tipping-hand":{"a":"Person Tipping Hand","b":"1F481","j":["hand","help","information","sassy","tipping"]},"man-tipping-hand":{"a":"Man Tipping Hand","b":"1F481-200D-2642-FE0F","j":["man","sassy","tipping hand","male","boy","human","information"]},"woman-tipping-hand":{"a":"Woman Tipping Hand","b":"1F481-200D-2640-FE0F","j":["sassy","tipping hand","woman","female","girl","human","information"]},"person-raising-hand":{"a":"Person Raising Hand","b":"1F64B","j":["gesture","hand","happy","raised","question"]},"man-raising-hand":{"a":"Man Raising Hand","b":"1F64B-200D-2642-FE0F","j":["gesture","man","raising hand","male","boy"]},"woman-raising-hand":{"a":"Woman Raising Hand","b":"1F64B-200D-2640-FE0F","j":["gesture","raising hand","woman","female","girl"]},"deaf-person":{"a":"Deaf Person","b":"1F9CF","j":["accessibility","deaf","ear","hear"]},"deaf-man":{"a":"Deaf Man","b":"1F9CF-200D-2642-FE0F","j":["deaf","man","accessibility"]},"deaf-woman":{"a":"Deaf Woman","b":"1F9CF-200D-2640-FE0F","j":["deaf","woman","accessibility"]},"person-bowing":{"a":"Person Bowing","b":"1F647","j":["apology","bow","gesture","sorry","respectiful"]},"man-bowing":{"a":"Man Bowing","b":"1F647-200D-2642-FE0F","j":["apology","bowing","favor","gesture","man","sorry","male","boy"]},"woman-bowing":{"a":"Woman Bowing","b":"1F647-200D-2640-FE0F","j":["apology","bowing","favor","gesture","sorry","woman","female","girl"]},"person-facepalming":{"a":"Person Facepalming","b":"1F926","j":["disbelief","exasperation","face","palm","disappointed"]},"man-facepalming":{"a":"Man Facepalming","b":"1F926-200D-2642-FE0F","j":["disbelief","exasperation","facepalm","man","male","boy"]},"woman-facepalming":{"a":"Woman Facepalming","b":"1F926-200D-2640-FE0F","j":["disbelief","exasperation","facepalm","woman","female","girl"]},"person-shrugging":{"a":"Person Shrugging","b":"1F937","j":["doubt","ignorance","indifference","shrug","regardless"]},"man-shrugging":{"a":"Man Shrugging","b":"1F937-200D-2642-FE0F","j":["doubt","ignorance","indifference","man","shrug","male","boy","confused","indifferent"]},"woman-shrugging":{"a":"Woman Shrugging","b":"1F937-200D-2640-FE0F","j":["doubt","ignorance","indifference","shrug","woman","female","girl","confused","indifferent"]},"health-worker":{"a":"Health Worker","b":"1F9D1-200D-2695-FE0F","j":["doctor","healthcare","nurse","therapist","hospital"]},"man-health-worker":{"a":"Man Health Worker","b":"1F468-200D-2695-FE0F","j":["doctor","healthcare","man","nurse","therapist","human"]},"woman-health-worker":{"a":"Woman Health Worker","b":"1F469-200D-2695-FE0F","j":["doctor","healthcare","nurse","therapist","woman","human"]},"student":{"a":"Student","b":"1F9D1-200D-1F393","j":["graduate","learn"]},"man-student":{"a":"Man Student","b":"1F468-200D-1F393","j":["graduate","man","student","human"]},"woman-student":{"a":"Woman Student","b":"1F469-200D-1F393","j":["graduate","student","woman","human"]},"teacher":{"a":"Teacher","b":"1F9D1-200D-1F3EB","j":["instructor","lecturer","professor"]},"man-teacher":{"a":"Man Teacher","b":"1F468-200D-1F3EB","j":["instructor","lecturer","man","professor","teacher","human"]},"woman-teacher":{"a":"Woman Teacher","b":"1F469-200D-1F3EB","j":["instructor","lecturer","professor","teacher","woman","human"]},"judge":{"a":"Judge","b":"1F9D1-200D-2696-FE0F","j":["justice","law","scales"]},"man-judge":{"a":"Man Judge","b":"1F468-200D-2696-FE0F","j":["judge","justice","law","man","scales","court","human"]},"woman-judge":{"a":"Woman Judge","b":"1F469-200D-2696-FE0F","j":["judge","justice","law","scales","woman","court","human"]},"farmer":{"a":"Farmer","b":"1F9D1-200D-1F33E","j":["gardener","rancher","crops"]},"man-farmer":{"a":"Man Farmer","b":"1F468-200D-1F33E","j":["farmer","gardener","man","rancher","human"]},"woman-farmer":{"a":"Woman Farmer","b":"1F469-200D-1F33E","j":["farmer","gardener","rancher","woman","human"]},"cook":{"a":"Cook","b":"1F9D1-200D-1F373","j":["chef","food","kitchen","culinary"]},"man-cook":{"a":"Man Cook","b":"1F468-200D-1F373","j":["chef","cook","man","human"]},"woman-cook":{"a":"Woman Cook","b":"1F469-200D-1F373","j":["chef","cook","woman","human"]},"mechanic":{"a":"Mechanic","b":"1F9D1-200D-1F527","j":["electrician","plumber","tradesperson","worker","technician"]},"man-mechanic":{"a":"Man Mechanic","b":"1F468-200D-1F527","j":["electrician","man","mechanic","plumber","tradesperson","human","wrench"]},"woman-mechanic":{"a":"Woman Mechanic","b":"1F469-200D-1F527","j":["electrician","mechanic","plumber","tradesperson","woman","human","wrench"]},"factory-worker":{"a":"Factory Worker","b":"1F9D1-200D-1F3ED","j":["assembly","factory","industrial","worker","labor"]},"man-factory-worker":{"a":"Man Factory Worker","b":"1F468-200D-1F3ED","j":["assembly","factory","industrial","man","worker","human"]},"woman-factory-worker":{"a":"Woman Factory Worker","b":"1F469-200D-1F3ED","j":["assembly","factory","industrial","woman","worker","human"]},"office-worker":{"a":"Office Worker","b":"1F9D1-200D-1F4BC","j":["architect","business","manager","white-collar"]},"man-office-worker":{"a":"Man Office Worker","b":"1F468-200D-1F4BC","j":["architect","business","man","manager","white-collar","human"]},"woman-office-worker":{"a":"Woman Office Worker","b":"1F469-200D-1F4BC","j":["architect","business","manager","white-collar","woman","human"]},"scientist":{"a":"Scientist","b":"1F9D1-200D-1F52C","j":["biologist","chemist","engineer","physicist","chemistry"]},"man-scientist":{"a":"Man Scientist","b":"1F468-200D-1F52C","j":["biologist","chemist","engineer","man","physicist","scientist","human"]},"woman-scientist":{"a":"Woman Scientist","b":"1F469-200D-1F52C","j":["biologist","chemist","engineer","physicist","scientist","woman","human"]},"technologist":{"a":"Technologist","b":"1F9D1-200D-1F4BB","j":["coder","developer","inventor","software","computer"]},"man-technologist":{"a":"Man Technologist","b":"1F468-200D-1F4BB","j":["coder","developer","inventor","man","software","technologist","engineer","programmer","human","laptop","computer"]},"woman-technologist":{"a":"Woman Technologist","b":"1F469-200D-1F4BB","j":["coder","developer","inventor","software","technologist","woman","engineer","programmer","human","laptop","computer"]},"singer":{"a":"Singer","b":"1F9D1-200D-1F3A4","j":["actor","entertainer","rock","star","song","artist","performer"]},"man-singer":{"a":"Man Singer","b":"1F468-200D-1F3A4","j":["actor","entertainer","man","rock","singer","star","rockstar","human"]},"woman-singer":{"a":"Woman Singer","b":"1F469-200D-1F3A4","j":["actor","entertainer","rock","singer","star","woman","rockstar","human"]},"artist":{"a":"Artist","b":"1F9D1-200D-1F3A8","j":["palette","painting","draw","creativity"]},"man-artist":{"a":"Man Artist","b":"1F468-200D-1F3A8","j":["artist","man","palette","painter","human"]},"woman-artist":{"a":"Woman Artist","b":"1F469-200D-1F3A8","j":["artist","palette","woman","painter","human"]},"pilot":{"a":"Pilot","b":"1F9D1-200D-2708-FE0F","j":["plane","fly","airplane"]},"man-pilot":{"a":"Man Pilot","b":"1F468-200D-2708-FE0F","j":["man","pilot","plane","aviator","human"]},"woman-pilot":{"a":"Woman Pilot","b":"1F469-200D-2708-FE0F","j":["pilot","plane","woman","aviator","human"]},"astronaut":{"a":"Astronaut","b":"1F9D1-200D-1F680","j":["rocket","outerspace"]},"man-astronaut":{"a":"Man Astronaut","b":"1F468-200D-1F680","j":["astronaut","man","rocket","space","human"]},"woman-astronaut":{"a":"Woman Astronaut","b":"1F469-200D-1F680","j":["astronaut","rocket","woman","space","human"]},"firefighter":{"a":"Firefighter","b":"1F9D1-200D-1F692","j":["fire","firetruck"]},"man-firefighter":{"a":"Man Firefighter","b":"1F468-200D-1F692","j":["firefighter","firetruck","man","fireman","human"]},"woman-firefighter":{"a":"Woman Firefighter","b":"1F469-200D-1F692","j":["firefighter","firetruck","woman","fireman","human"]},"police-officer":{"a":"Police Officer","b":"1F46E","j":["cop","officer","police"]},"man-police-officer":{"a":"Man Police Officer","b":"1F46E-200D-2642-FE0F","j":["cop","man","officer","police","law","legal","enforcement","arrest","911"]},"woman-police-officer":{"a":"Woman Police Officer","b":"1F46E-200D-2640-FE0F","j":["cop","officer","police","woman","law","legal","enforcement","arrest","911","female"]},"detective":{"a":"Detective","b":"1F575-FE0F","j":["sleuth","spy","human"]},"man-detective":{"a":"Man Detective","b":"1F575-FE0F-200D-2642-FE0F","j":["detective","man","sleuth","spy","crime"]},"woman-detective":{"a":"Woman Detective","b":"1F575-FE0F-200D-2640-FE0F","j":["detective","sleuth","spy","woman","human","female"]},"guard":{"a":"Guard","b":"1F482","j":["protect"]},"man-guard":{"a":"Man Guard","b":"1F482-200D-2642-FE0F","j":["guard","man","uk","gb","british","male","guy","royal"]},"woman-guard":{"a":"Woman Guard","b":"1F482-200D-2640-FE0F","j":["guard","woman","uk","gb","british","female","royal"]},"ninja":{"a":"Ninja","b":"1F977","j":["fighter","hidden","stealth","ninjutsu","skills","japanese"]},"construction-worker":{"a":"Construction Worker","b":"1F477","j":["construction","hat","worker","labor","build"]},"man-construction-worker":{"a":"Man Construction Worker","b":"1F477-200D-2642-FE0F","j":["construction","man","worker","male","human","wip","guy","build","labor"]},"woman-construction-worker":{"a":"Woman Construction Worker","b":"1F477-200D-2640-FE0F","j":["construction","woman","worker","female","human","wip","build","labor"]},"person-with-crown":{"a":"Person with Crown","b":"1FAC5","j":["monarch","noble","regal","royalty","power"]},"prince":{"a":"Prince","b":"1F934","j":["boy","man","male","crown","royal","king"]},"princess":{"a":"Princess","b":"1F478","j":["fairy tale","fantasy","girl","woman","female","blond","crown","royal","queen"]},"person-wearing-turban":{"a":"Person Wearing Turban","b":"1F473","j":["turban","headdress"]},"man-wearing-turban":{"a":"Man Wearing Turban","b":"1F473-200D-2642-FE0F","j":["man","turban","male","indian","hinduism","arabs"]},"woman-wearing-turban":{"a":"Woman Wearing Turban","b":"1F473-200D-2640-FE0F","j":["turban","woman","female","indian","hinduism","arabs"]},"person-with-skullcap":{"a":"Person with Skullcap","b":"1F472","j":["cap","gua pi mao","hat","person","skullcap","man_with_skullcap","male","boy","chinese"]},"woman-with-headscarf":{"a":"Woman with Headscarf","b":"1F9D5","j":["headscarf","hijab","mantilla","tichel","bandana","head kerchief","female"]},"person-in-tuxedo":{"a":"Person in Tuxedo","b":"1F935","j":["groom","person","tuxedo","man_in_tuxedo","couple","marriage","wedding"]},"man-in-tuxedo":{"a":"Man in Tuxedo","b":"1F935-200D-2642-FE0F","j":["man","tuxedo","formal","fashion"]},"woman-in-tuxedo":{"a":"Woman in Tuxedo","b":"1F935-200D-2640-FE0F","j":["tuxedo","woman","formal","fashion"]},"person-with-veil":{"a":"Person with Veil","b":"1F470","j":["bride","person","veil","wedding","bride_with_veil","couple","marriage","woman"]},"man-with-veil":{"a":"Man with Veil","b":"1F470-200D-2642-FE0F","j":["man","veil","wedding","marriage"]},"woman-with-veil":{"a":"Woman with Veil","b":"1F470-200D-2640-FE0F","j":["veil","woman","wedding","marriage"]},"pregnant-woman":{"a":"Pregnant Woman","b":"1F930","j":["pregnant","woman","baby"]},"pregnant-man":{"a":"Pregnant Man","b":"1FAC3","j":["belly","bloated","full","pregnant","baby"]},"pregnant-person":{"a":"Pregnant Person","b":"1FAC4","j":["belly","bloated","full","pregnant","baby"]},"breastfeeding":{"a":"Breast-Feeding","b":"1F931","j":["baby","breast","breast-feeding","nursing","breast_feeding"]},"woman-feeding-baby":{"a":"Woman Feeding Baby","b":"1F469-200D-1F37C","j":["baby","feeding","nursing","woman","birth","food"]},"man-feeding-baby":{"a":"Man Feeding Baby","b":"1F468-200D-1F37C","j":["baby","feeding","man","nursing","birth","food"]},"person-feeding-baby":{"a":"Person Feeding Baby","b":"1F9D1-200D-1F37C","j":["baby","feeding","nursing","person","birth","food"]},"baby-angel":{"a":"Baby Angel","b":"1F47C","j":["angel","baby","face","fairy tale","fantasy","heaven","wings","halo"]},"santa-claus":{"a":"Santa Claus","b":"1F385","j":["celebration","Christmas","claus","father","santa","festival","man","male","xmas","father christmas"]},"mrs-claus":{"a":"Mrs. Claus","b":"1F936","j":["celebration","Christmas","claus","mother","Mrs.","woman","female","xmas","mother christmas"]},"mx-claus":{"a":"Mx Claus","b":"1F9D1-200D-1F384","j":["christmas","claus"]},"superhero":{"a":"Superhero","b":"1F9B8","j":["good","hero","heroine","superpower","marvel"]},"man-superhero":{"a":"Man Superhero","b":"1F9B8-200D-2642-FE0F","j":["good","hero","man","superpower","male","superpowers"]},"woman-superhero":{"a":"Woman Superhero","b":"1F9B8-200D-2640-FE0F","j":["good","hero","heroine","superpower","woman","female","superpowers"]},"supervillain":{"a":"Supervillain","b":"1F9B9","j":["criminal","evil","superpower","villain","marvel"]},"man-supervillain":{"a":"Man Supervillain","b":"1F9B9-200D-2642-FE0F","j":["criminal","evil","man","superpower","villain","male","bad","hero","superpowers"]},"woman-supervillain":{"a":"Woman Supervillain","b":"1F9B9-200D-2640-FE0F","j":["criminal","evil","superpower","villain","woman","female","bad","heroine","superpowers"]},"mage":{"a":"Mage","b":"1F9D9","j":["sorcerer","sorceress","witch","wizard","magic"]},"man-mage":{"a":"Man Mage","b":"1F9D9-200D-2642-FE0F","j":["sorcerer","wizard","man","male","mage"]},"woman-mage":{"a":"Woman Mage","b":"1F9D9-200D-2640-FE0F","j":["sorceress","witch","woman","female","mage"]},"fairy":{"a":"Fairy","b":"1F9DA","j":["Oberon","Puck","Titania","wings","magical"]},"man-fairy":{"a":"Man Fairy","b":"1F9DA-200D-2642-FE0F","j":["Oberon","Puck","man","male"]},"woman-fairy":{"a":"Woman Fairy","b":"1F9DA-200D-2640-FE0F","j":["Titania","woman","female"]},"vampire":{"a":"Vampire","b":"1F9DB","j":["Dracula","undead","blood","twilight"]},"man-vampire":{"a":"Man Vampire","b":"1F9DB-200D-2642-FE0F","j":["Dracula","undead","man","male","dracula"]},"woman-vampire":{"a":"Woman Vampire","b":"1F9DB-200D-2640-FE0F","j":["undead","woman","female"]},"merperson":{"a":"Merperson","b":"1F9DC","j":["mermaid","merman","merwoman","sea"]},"merman":{"a":"Merman","b":"1F9DC-200D-2642-FE0F","j":["Triton","man","male","triton"]},"mermaid":{"a":"Mermaid","b":"1F9DC-200D-2640-FE0F","j":["merwoman","woman","female","ariel"]},"elf":{"a":"Elf","b":"1F9DD","j":["magical","LOTR style"]},"man-elf":{"a":"Man Elf","b":"1F9DD-200D-2642-FE0F","j":["magical","man","male"]},"woman-elf":{"a":"Woman Elf","b":"1F9DD-200D-2640-FE0F","j":["magical","woman","female"]},"genie":{"a":"Genie","b":"1F9DE","j":["djinn","(non-human color)","magical","wishes"]},"man-genie":{"a":"Man Genie","b":"1F9DE-200D-2642-FE0F","j":["djinn","man","male"]},"woman-genie":{"a":"Woman Genie","b":"1F9DE-200D-2640-FE0F","j":["djinn","woman","female"]},"zombie":{"a":"Zombie","b":"1F9DF","j":["undead","walking dead","(non-human color)","dead"]},"man-zombie":{"a":"Man Zombie","b":"1F9DF-200D-2642-FE0F","j":["undead","walking dead","man","male","dracula"]},"woman-zombie":{"a":"Woman Zombie","b":"1F9DF-200D-2640-FE0F","j":["undead","walking dead","woman","female"]},"troll":{"a":"Troll","b":"1F9CC","j":["fairy tale","fantasy","monster","mystical"]},"person-getting-massage":{"a":"Person Getting Massage","b":"1F486","j":["face","massage","salon","relax"]},"man-getting-massage":{"a":"Man Getting Massage","b":"1F486-200D-2642-FE0F","j":["face","man","massage","male","boy","head"]},"woman-getting-massage":{"a":"Woman Getting Massage","b":"1F486-200D-2640-FE0F","j":["face","massage","woman","female","girl","head"]},"person-getting-haircut":{"a":"Person Getting Haircut","b":"1F487","j":["barber","beauty","haircut","parlor","hairstyle"]},"man-getting-haircut":{"a":"Man Getting Haircut","b":"1F487-200D-2642-FE0F","j":["haircut","man","male","boy"]},"woman-getting-haircut":{"a":"Woman Getting Haircut","b":"1F487-200D-2640-FE0F","j":["haircut","woman","female","girl"]},"person-walking":{"a":"Person Walking","b":"1F6B6","j":["hike","walk","walking","move"]},"man-walking":{"a":"Man Walking","b":"1F6B6-200D-2642-FE0F","j":["hike","man","walk","human","feet","steps"]},"woman-walking":{"a":"Woman Walking","b":"1F6B6-200D-2640-FE0F","j":["hike","walk","woman","human","feet","steps","female"]},"person-walking-facing-right":{"a":"⊛ Person Walking Facing Right","b":"1F6B6-200D-27A1-FE0F","j":[""]},"woman-walking-facing-right":{"a":"⊛ Woman Walking Facing Right","b":"1F6B6-200D-2640-FE0F-200D-27A1-FE0F","j":[""]},"man-walking-facing-right":{"a":"⊛ Man Walking Facing Right","b":"1F6B6-200D-2642-FE0F-200D-27A1-FE0F","j":[""]},"person-standing":{"a":"Person Standing","b":"1F9CD","j":["stand","standing","still"]},"man-standing":{"a":"Man Standing","b":"1F9CD-200D-2642-FE0F","j":["man","standing","still"]},"woman-standing":{"a":"Woman Standing","b":"1F9CD-200D-2640-FE0F","j":["standing","woman","still"]},"person-kneeling":{"a":"Person Kneeling","b":"1F9CE","j":["kneel","kneeling","pray","respectful"]},"man-kneeling":{"a":"Man Kneeling","b":"1F9CE-200D-2642-FE0F","j":["kneeling","man","pray","respectful"]},"woman-kneeling":{"a":"Woman Kneeling","b":"1F9CE-200D-2640-FE0F","j":["kneeling","woman","respectful","pray"]},"person-kneeling-facing-right":{"a":"⊛ Person Kneeling Facing Right","b":"1F9CE-200D-27A1-FE0F","j":[""]},"woman-kneeling-facing-right":{"a":"⊛ Woman Kneeling Facing Right","b":"1F9CE-200D-2640-FE0F-200D-27A1-FE0F","j":[""]},"man-kneeling-facing-right":{"a":"⊛ Man Kneeling Facing Right","b":"1F9CE-200D-2642-FE0F-200D-27A1-FE0F","j":[""]},"person-with-white-cane":{"a":"Person with White Cane","b":"1F9D1-200D-1F9AF","j":["accessibility","blind","person_with_probing_cane"]},"person-with-white-cane-facing-right":{"a":"⊛ Person with White Cane Facing Right","b":"1F9D1-200D-1F9AF-200D-27A1-FE0F","j":[""]},"man-with-white-cane":{"a":"Man with White Cane","b":"1F468-200D-1F9AF","j":["accessibility","blind","man","man_with_probing_cane"]},"man-with-white-cane-facing-right":{"a":"⊛ Man with White Cane Facing Right","b":"1F468-200D-1F9AF-200D-27A1-FE0F","j":[""]},"woman-with-white-cane":{"a":"Woman with White Cane","b":"1F469-200D-1F9AF","j":["accessibility","blind","woman","woman_with_probing_cane"]},"woman-with-white-cane-facing-right":{"a":"⊛ Woman with White Cane Facing Right","b":"1F469-200D-1F9AF-200D-27A1-FE0F","j":[""]},"person-in-motorized-wheelchair":{"a":"Person in Motorized Wheelchair","b":"1F9D1-200D-1F9BC","j":["accessibility","wheelchair","disability"]},"person-in-motorized-wheelchair-facing-right":{"a":"⊛ Person in Motorized Wheelchair Facing Right","b":"1F9D1-200D-1F9BC-200D-27A1-FE0F","j":[""]},"man-in-motorized-wheelchair":{"a":"Man in Motorized Wheelchair","b":"1F468-200D-1F9BC","j":["accessibility","man","wheelchair","disability"]},"man-in-motorized-wheelchair-facing-right":{"a":"⊛ Man in Motorized Wheelchair Facing Right","b":"1F468-200D-1F9BC-200D-27A1-FE0F","j":[""]},"woman-in-motorized-wheelchair":{"a":"Woman in Motorized Wheelchair","b":"1F469-200D-1F9BC","j":["accessibility","wheelchair","woman","disability"]},"woman-in-motorized-wheelchair-facing-right":{"a":"⊛ Woman in Motorized Wheelchair Facing Right","b":"1F469-200D-1F9BC-200D-27A1-FE0F","j":[""]},"person-in-manual-wheelchair":{"a":"Person in Manual Wheelchair","b":"1F9D1-200D-1F9BD","j":["accessibility","wheelchair","disability"]},"person-in-manual-wheelchair-facing-right":{"a":"⊛ Person in Manual Wheelchair Facing Right","b":"1F9D1-200D-1F9BD-200D-27A1-FE0F","j":[""]},"man-in-manual-wheelchair":{"a":"Man in Manual Wheelchair","b":"1F468-200D-1F9BD","j":["accessibility","man","wheelchair","disability"]},"man-in-manual-wheelchair-facing-right":{"a":"⊛ Man in Manual Wheelchair Facing Right","b":"1F468-200D-1F9BD-200D-27A1-FE0F","j":[""]},"woman-in-manual-wheelchair":{"a":"Woman in Manual Wheelchair","b":"1F469-200D-1F9BD","j":["accessibility","wheelchair","woman","disability"]},"woman-in-manual-wheelchair-facing-right":{"a":"⊛ Woman in Manual Wheelchair Facing Right","b":"1F469-200D-1F9BD-200D-27A1-FE0F","j":[""]},"person-running":{"a":"Person Running","b":"1F3C3","j":["marathon","running","move"]},"man-running":{"a":"Man Running","b":"1F3C3-200D-2642-FE0F","j":["man","marathon","racing","running","walking","exercise","race"]},"woman-running":{"a":"Woman Running","b":"1F3C3-200D-2640-FE0F","j":["marathon","racing","running","woman","walking","exercise","race","female"]},"person-running-facing-right":{"a":"⊛ Person Running Facing Right","b":"1F3C3-200D-27A1-FE0F","j":[""]},"woman-running-facing-right":{"a":"⊛ Woman Running Facing Right","b":"1F3C3-200D-2640-FE0F-200D-27A1-FE0F","j":[""]},"man-running-facing-right":{"a":"⊛ Man Running Facing Right","b":"1F3C3-200D-2642-FE0F-200D-27A1-FE0F","j":[""]},"woman-dancing":{"a":"Woman Dancing","b":"1F483","j":["dance","dancing","woman","female","girl","fun"]},"man-dancing":{"a":"Man Dancing","b":"1F57A","j":["dance","dancing","man","male","boy","fun","dancer"]},"person-in-suit-levitating":{"a":"Person in Suit Levitating","b":"1F574-FE0F","j":["business","person","suit","man_in_suit_levitating","levitate","hover","jump"]},"people-with-bunny-ears":{"a":"People with Bunny Ears","b":"1F46F","j":["bunny ear","dancer","partying","perform","costume"]},"men-with-bunny-ears":{"a":"Men with Bunny Ears","b":"1F46F-200D-2642-FE0F","j":["bunny ear","dancer","men","partying","male","bunny","boys"]},"women-with-bunny-ears":{"a":"Women with Bunny Ears","b":"1F46F-200D-2640-FE0F","j":["bunny ear","dancer","partying","women","female","bunny","girls"]},"person-in-steamy-room":{"a":"Person in Steamy Room","b":"1F9D6","j":["sauna","steam room","hamam","steambath","relax","spa"]},"man-in-steamy-room":{"a":"Man in Steamy Room","b":"1F9D6-200D-2642-FE0F","j":["sauna","steam room","male","man","spa","steamroom"]},"woman-in-steamy-room":{"a":"Woman in Steamy Room","b":"1F9D6-200D-2640-FE0F","j":["sauna","steam room","female","woman","spa","steamroom"]},"person-climbing":{"a":"Person Climbing","b":"1F9D7","j":["climber","sport"]},"man-climbing":{"a":"Man Climbing","b":"1F9D7-200D-2642-FE0F","j":["climber","sports","hobby","man","male","rock"]},"woman-climbing":{"a":"Woman Climbing","b":"1F9D7-200D-2640-FE0F","j":["climber","sports","hobby","woman","female","rock"]},"person-fencing":{"a":"Person Fencing","b":"1F93A","j":["fencer","fencing","sword","sports"]},"horse-racing":{"a":"Horse Racing","b":"1F3C7","j":["horse","jockey","racehorse","racing","animal","betting","competition","gambling","luck"]},"skier":{"a":"Skier","b":"26F7-FE0F","j":["ski","snow","sports","winter"]},"snowboarder":{"a":"Snowboarder","b":"1F3C2-FE0F","j":["ski","snow","snowboard","sports","winter"]},"person-golfing":{"a":"Person Golfing","b":"1F3CC-FE0F","j":["ball","golf","sports","business"]},"man-golfing":{"a":"Man Golfing","b":"1F3CC-FE0F-200D-2642-FE0F","j":["golf","man","sport"]},"woman-golfing":{"a":"Woman Golfing","b":"1F3CC-FE0F-200D-2640-FE0F","j":["golf","woman","sports","business","female"]},"person-surfing":{"a":"Person Surfing","b":"1F3C4-FE0F","j":["surfing","sport","sea"]},"man-surfing":{"a":"Man Surfing","b":"1F3C4-200D-2642-FE0F","j":["man","surfing","sports","ocean","sea","summer","beach"]},"woman-surfing":{"a":"Woman Surfing","b":"1F3C4-200D-2640-FE0F","j":["surfing","woman","sports","ocean","sea","summer","beach","female"]},"person-rowing-boat":{"a":"Person Rowing Boat","b":"1F6A3","j":["boat","rowboat","sport","move"]},"man-rowing-boat":{"a":"Man Rowing Boat","b":"1F6A3-200D-2642-FE0F","j":["boat","man","rowboat","sports","hobby","water","ship"]},"woman-rowing-boat":{"a":"Woman Rowing Boat","b":"1F6A3-200D-2640-FE0F","j":["boat","rowboat","woman","sports","hobby","water","ship","female"]},"person-swimming":{"a":"Person Swimming","b":"1F3CA-FE0F","j":["swim","sport","pool"]},"man-swimming":{"a":"Man Swimming","b":"1F3CA-200D-2642-FE0F","j":["man","swim","sports","exercise","human","athlete","water","summer"]},"woman-swimming":{"a":"Woman Swimming","b":"1F3CA-200D-2640-FE0F","j":["swim","woman","sports","exercise","human","athlete","water","summer","female"]},"person-bouncing-ball":{"a":"Person Bouncing Ball","b":"26F9-FE0F","j":["ball","sports","human"]},"man-bouncing-ball":{"a":"Man Bouncing Ball","b":"26F9-FE0F-200D-2642-FE0F","j":["ball","man","sport"]},"woman-bouncing-ball":{"a":"Woman Bouncing Ball","b":"26F9-FE0F-200D-2640-FE0F","j":["ball","woman","sports","human","female"]},"person-lifting-weights":{"a":"Person Lifting Weights","b":"1F3CB-FE0F","j":["lifter","weight","sports","training","exercise"]},"man-lifting-weights":{"a":"Man Lifting Weights","b":"1F3CB-FE0F-200D-2642-FE0F","j":["man","weight lifter","sport"]},"woman-lifting-weights":{"a":"Woman Lifting Weights","b":"1F3CB-FE0F-200D-2640-FE0F","j":["weight lifter","woman","sports","training","exercise","female"]},"person-biking":{"a":"Person Biking","b":"1F6B4","j":["bicycle","biking","cyclist","bike","sport","move"]},"man-biking":{"a":"Man Biking","b":"1F6B4-200D-2642-FE0F","j":["bicycle","biking","cyclist","man","bike","sports","exercise","hipster"]},"woman-biking":{"a":"Woman Biking","b":"1F6B4-200D-2640-FE0F","j":["bicycle","biking","cyclist","woman","bike","sports","exercise","hipster","female"]},"person-mountain-biking":{"a":"Person Mountain Biking","b":"1F6B5","j":["bicycle","bicyclist","bike","cyclist","mountain","sport","move"]},"man-mountain-biking":{"a":"Man Mountain Biking","b":"1F6B5-200D-2642-FE0F","j":["bicycle","bike","cyclist","man","mountain","transportation","sports","human","race"]},"woman-mountain-biking":{"a":"Woman Mountain Biking","b":"1F6B5-200D-2640-FE0F","j":["bicycle","bike","biking","cyclist","mountain","woman","transportation","sports","human","race","female"]},"person-cartwheeling":{"a":"Person Cartwheeling","b":"1F938","j":["cartwheel","gymnastics","sport","gymnastic"]},"man-cartwheeling":{"a":"Man Cartwheeling","b":"1F938-200D-2642-FE0F","j":["cartwheel","gymnastics","man"]},"woman-cartwheeling":{"a":"Woman Cartwheeling","b":"1F938-200D-2640-FE0F","j":["cartwheel","gymnastics","woman"]},"people-wrestling":{"a":"People Wrestling","b":"1F93C","j":["wrestle","wrestler","sport"]},"men-wrestling":{"a":"Men Wrestling","b":"1F93C-200D-2642-FE0F","j":["men","wrestle","sports","wrestlers"]},"women-wrestling":{"a":"Women Wrestling","b":"1F93C-200D-2640-FE0F","j":["women","wrestle","sports","wrestlers"]},"person-playing-water-polo":{"a":"Person Playing Water Polo","b":"1F93D","j":["polo","water","sport"]},"man-playing-water-polo":{"a":"Man Playing Water Polo","b":"1F93D-200D-2642-FE0F","j":["man","water polo","sports","pool"]},"woman-playing-water-polo":{"a":"Woman Playing Water Polo","b":"1F93D-200D-2640-FE0F","j":["water polo","woman","sports","pool"]},"person-playing-handball":{"a":"Person Playing Handball","b":"1F93E","j":["ball","handball","sport"]},"man-playing-handball":{"a":"Man Playing Handball","b":"1F93E-200D-2642-FE0F","j":["handball","man","sports"]},"woman-playing-handball":{"a":"Woman Playing Handball","b":"1F93E-200D-2640-FE0F","j":["handball","woman","sports"]},"person-juggling":{"a":"Person Juggling","b":"1F939","j":["balance","juggle","multitask","skill","performance"]},"man-juggling":{"a":"Man Juggling","b":"1F939-200D-2642-FE0F","j":["juggling","man","multitask","juggle","balance","skill"]},"woman-juggling":{"a":"Woman Juggling","b":"1F939-200D-2640-FE0F","j":["juggling","multitask","woman","juggle","balance","skill"]},"person-in-lotus-position":{"a":"Person in Lotus Position","b":"1F9D8","j":["meditation","yoga","serenity","meditate"]},"man-in-lotus-position":{"a":"Man in Lotus Position","b":"1F9D8-200D-2642-FE0F","j":["meditation","yoga","man","male","serenity","zen","mindfulness"]},"woman-in-lotus-position":{"a":"Woman in Lotus Position","b":"1F9D8-200D-2640-FE0F","j":["meditation","yoga","woman","female","serenity","zen","mindfulness"]},"person-taking-bath":{"a":"Person Taking Bath","b":"1F6C0","j":["bath","bathtub","clean","shower","bathroom"]},"person-in-bed":{"a":"Person in Bed","b":"1F6CC","j":["good night","hotel","sleep","bed","rest"]},"people-holding-hands":{"a":"People Holding Hands","b":"1F9D1-200D-1F91D-200D-1F9D1","j":["couple","hand","hold","holding hands","person","friendship"]},"women-holding-hands":{"a":"Women Holding Hands","b":"1F46D","j":["couple","hand","holding hands","women","pair","friendship","love","like","female","people","human"]},"woman-and-man-holding-hands":{"a":"Woman and Man Holding Hands","b":"1F46B","j":["couple","hand","hold","holding hands","man","woman","pair","people","human","love","date","dating","like","affection","valentines","marriage"]},"men-holding-hands":{"a":"Men Holding Hands","b":"1F46C","j":["couple","Gemini","holding hands","man","men","twins","zodiac","pair","love","like","bromance","friendship","people","human"]},"kiss":{"a":"Kiss","b":"1F48F","j":["couple","pair","valentines","love","like","dating","marriage"]},"kiss-woman-man":{"a":"Kiss: Woman, Man","b":"1F469-200D-2764-FE0F-200D-1F48B-200D-1F468","j":["couple","kiss","man","woman","love"]},"kiss-man-man":{"a":"Kiss: Man, Man","b":"1F468-200D-2764-FE0F-200D-1F48B-200D-1F468","j":["couple","kiss","man","pair","valentines","love","like","dating","marriage"]},"kiss-woman-woman":{"a":"Kiss: Woman, Woman","b":"1F469-200D-2764-FE0F-200D-1F48B-200D-1F469","j":["couple","kiss","woman","pair","valentines","love","like","dating","marriage"]},"couple-with-heart":{"a":"Couple with Heart","b":"1F491","j":["couple","love","pair","like","affection","human","dating","valentines","marriage"]},"couple-with-heart-woman-man":{"a":"Couple with Heart: Woman, Man","b":"1F469-200D-2764-FE0F-200D-1F468","j":["couple","couple with heart","love","man","woman"]},"couple-with-heart-man-man":{"a":"Couple with Heart: Man, Man","b":"1F468-200D-2764-FE0F-200D-1F468","j":["couple","couple with heart","love","man","pair","like","affection","human","dating","valentines","marriage"]},"couple-with-heart-woman-woman":{"a":"Couple with Heart: Woman, Woman","b":"1F469-200D-2764-FE0F-200D-1F469","j":["couple","couple with heart","love","woman","pair","like","affection","human","dating","valentines","marriage"]},"family-man-woman-boy":{"a":"Family: Man, Woman, Boy","b":"1F468-200D-1F469-200D-1F466","j":["boy","family","man","woman","love"]},"family-man-woman-girl":{"a":"Family: Man, Woman, Girl","b":"1F468-200D-1F469-200D-1F467","j":["family","girl","man","woman","home","parents","people","human","child"]},"family-man-woman-girl-boy":{"a":"Family: Man, Woman, Girl, Boy","b":"1F468-200D-1F469-200D-1F467-200D-1F466","j":["boy","family","girl","man","woman","home","parents","people","human","children"]},"family-man-woman-boy-boy":{"a":"Family: Man, Woman, Boy, Boy","b":"1F468-200D-1F469-200D-1F466-200D-1F466","j":["boy","family","man","woman","home","parents","people","human","children"]},"family-man-woman-girl-girl":{"a":"Family: Man, Woman, Girl, Girl","b":"1F468-200D-1F469-200D-1F467-200D-1F467","j":["family","girl","man","woman","home","parents","people","human","children"]},"family-man-man-boy":{"a":"Family: Man, Man, Boy","b":"1F468-200D-1F468-200D-1F466","j":["boy","family","man","home","parents","people","human","children"]},"family-man-man-girl":{"a":"Family: Man, Man, Girl","b":"1F468-200D-1F468-200D-1F467","j":["family","girl","man","home","parents","people","human","children"]},"family-man-man-girl-boy":{"a":"Family: Man, Man, Girl, Boy","b":"1F468-200D-1F468-200D-1F467-200D-1F466","j":["boy","family","girl","man","home","parents","people","human","children"]},"family-man-man-boy-boy":{"a":"Family: Man, Man, Boy, Boy","b":"1F468-200D-1F468-200D-1F466-200D-1F466","j":["boy","family","man","home","parents","people","human","children"]},"family-man-man-girl-girl":{"a":"Family: Man, Man, Girl, Girl","b":"1F468-200D-1F468-200D-1F467-200D-1F467","j":["family","girl","man","home","parents","people","human","children"]},"family-woman-woman-boy":{"a":"Family: Woman, Woman, Boy","b":"1F469-200D-1F469-200D-1F466","j":["boy","family","woman","home","parents","people","human","children"]},"family-woman-woman-girl":{"a":"Family: Woman, Woman, Girl","b":"1F469-200D-1F469-200D-1F467","j":["family","girl","woman","home","parents","people","human","children"]},"family-woman-woman-girl-boy":{"a":"Family: Woman, Woman, Girl, Boy","b":"1F469-200D-1F469-200D-1F467-200D-1F466","j":["boy","family","girl","woman","home","parents","people","human","children"]},"family-woman-woman-boy-boy":{"a":"Family: Woman, Woman, Boy, Boy","b":"1F469-200D-1F469-200D-1F466-200D-1F466","j":["boy","family","woman","home","parents","people","human","children"]},"family-woman-woman-girl-girl":{"a":"Family: Woman, Woman, Girl, Girl","b":"1F469-200D-1F469-200D-1F467-200D-1F467","j":["family","girl","woman","home","parents","people","human","children"]},"family-man-boy":{"a":"Family: Man, Boy","b":"1F468-200D-1F466","j":["boy","family","man","home","parent","people","human","child"]},"family-man-boy-boy":{"a":"Family: Man, Boy, Boy","b":"1F468-200D-1F466-200D-1F466","j":["boy","family","man","home","parent","people","human","children"]},"family-man-girl":{"a":"Family: Man, Girl","b":"1F468-200D-1F467","j":["family","girl","man","home","parent","people","human","child"]},"family-man-girl-boy":{"a":"Family: Man, Girl, Boy","b":"1F468-200D-1F467-200D-1F466","j":["boy","family","girl","man","home","parent","people","human","children"]},"family-man-girl-girl":{"a":"Family: Man, Girl, Girl","b":"1F468-200D-1F467-200D-1F467","j":["family","girl","man","home","parent","people","human","children"]},"family-woman-boy":{"a":"Family: Woman, Boy","b":"1F469-200D-1F466","j":["boy","family","woman","home","parent","people","human","child"]},"family-woman-boy-boy":{"a":"Family: Woman, Boy, Boy","b":"1F469-200D-1F466-200D-1F466","j":["boy","family","woman","home","parent","people","human","children"]},"family-woman-girl":{"a":"Family: Woman, Girl","b":"1F469-200D-1F467","j":["family","girl","woman","home","parent","people","human","child"]},"family-woman-girl-boy":{"a":"Family: Woman, Girl, Boy","b":"1F469-200D-1F467-200D-1F466","j":["boy","family","girl","woman","home","parent","people","human","children"]},"family-woman-girl-girl":{"a":"Family: Woman, Girl, Girl","b":"1F469-200D-1F467-200D-1F467","j":["family","girl","woman","home","parent","people","human","children"]},"speaking-head":{"a":"Speaking Head","b":"1F5E3-FE0F","j":["face","head","silhouette","speak","speaking","user","person","human","sing","say","talk"]},"bust-in-silhouette":{"a":"Bust in Silhouette","b":"1F464","j":["bust","silhouette","user","person","human"]},"busts-in-silhouette":{"a":"Busts in Silhouette","b":"1F465","j":["bust","silhouette","user","person","human","group","team"]},"people-hugging":{"a":"People Hugging","b":"1FAC2","j":["goodbye","hello","hug","thanks","care"]},"family":{"a":"Family","b":"1F46A-FE0F","j":["home","parents","child","mom","dad","father","mother","people","human"]},"family-adult-adult-child":{"a":"⊛ Family: Adult, Adult, Child","b":"1F9D1-200D-1F9D1-200D-1F9D2","j":["family: adult, adult, child"]},"family-adult-adult-child-child":{"a":"⊛ Family: Adult, Adult, Child, Child","b":"1F9D1-200D-1F9D1-200D-1F9D2-200D-1F9D2","j":["family: adult, adult, child, child"]},"family-adult-child":{"a":"⊛ Family: Adult, Child","b":"1F9D1-200D-1F9D2","j":["family: adult, child"]},"family-adult-child-child":{"a":"⊛ Family: Adult, Child, Child","b":"1F9D1-200D-1F9D2-200D-1F9D2","j":["family: adult, child, child"]},"footprints":{"a":"Footprints","b":"1F463","j":["clothing","footprint","print","feet","tracking","walking","beach"]},"red-hair":{"a":"Red Hair","b":"1F9B0","j":["ginger","red hair","redhead"]},"curly-hair":{"a":"Curly Hair","b":"1F9B1","j":["afro","curly","curly hair","ringlets"]},"white-hair":{"a":"White Hair","b":"1F9B3","j":["gray","hair","old","white"]},"bald":{"a":"Bald","b":"1F9B2","j":["bald","chemotherapy","hairless","no hair","shaven"]},"monkey-face":{"a":"Monkey Face","b":"1F435","j":["face","monkey","animal","nature","circus"]},"monkey":{"a":"Monkey","b":"1F412","j":["animal","nature","banana","circus"]},"gorilla":{"a":"Gorilla","b":"1F98D","j":["animal","nature","circus"]},"orangutan":{"a":"Orangutan","b":"1F9A7","j":["ape","animal"]},"dog-face":{"a":"Dog Face","b":"1F436","j":["dog","face","pet","animal","friend","nature","woof","puppy","faithful"]},"dog":{"a":"Dog","b":"1F415-FE0F","j":["pet","animal","nature","friend","doge","faithful"]},"guide-dog":{"a":"Guide Dog","b":"1F9AE","j":["accessibility","blind","guide","animal"]},"service-dog":{"a":"Service Dog","b":"1F415-200D-1F9BA","j":["accessibility","assistance","dog","service","blind","animal"]},"poodle":{"a":"Poodle","b":"1F429","j":["dog","animal","101","nature","pet"]},"wolf":{"a":"Wolf","b":"1F43A","j":["face","animal","nature","wild"]},"fox":{"a":"Fox","b":"1F98A","j":["face","animal","nature"]},"raccoon":{"a":"Raccoon","b":"1F99D","j":["curious","sly","animal","nature"]},"cat-face":{"a":"Cat Face","b":"1F431","j":["cat","face","pet","animal","meow","nature","kitten"]},"cat":{"a":"Cat","b":"1F408-FE0F","j":["pet","animal","meow","cats"]},"black-cat":{"a":"Black Cat","b":"1F408-200D-2B1B","j":["black","cat","unlucky","superstition","luck"]},"lion":{"a":"Lion","b":"1F981","j":["face","Leo","zodiac","animal","nature"]},"tiger-face":{"a":"Tiger Face","b":"1F42F","j":["face","tiger","animal","cat","danger","wild","nature","roar"]},"tiger":{"a":"Tiger","b":"1F405","j":["animal","nature","roar"]},"leopard":{"a":"Leopard","b":"1F406","j":["animal","nature"]},"horse-face":{"a":"Horse Face","b":"1F434","j":["face","horse","animal","brown","nature"]},"moose":{"a":"Moose","b":"1FACE","j":["animal","antlers","elk","mammal","shrek","canada","sweden","sven","cool"]},"donkey":{"a":"Donkey","b":"1FACF","j":["animal","ass","burro","mammal","mule","stubborn","eeyore"]},"horse":{"a":"Horse","b":"1F40E","j":["equestrian","racehorse","racing","animal","gamble","luck"]},"unicorn":{"a":"Unicorn","b":"1F984","j":["face","animal","nature","mystical"]},"zebra":{"a":"Zebra","b":"1F993","j":["stripe","animal","nature","stripes","safari"]},"deer":{"a":"Deer","b":"1F98C","j":["animal","nature","horns","venison"]},"bison":{"a":"Bison","b":"1F9AC","j":["buffalo","herd","wisent","ox"]},"cow-face":{"a":"Cow Face","b":"1F42E","j":["cow","face","beef","ox","animal","nature","moo","milk"]},"ox":{"a":"Ox","b":"1F402","j":["bull","Taurus","zodiac","animal","cow","beef"]},"water-buffalo":{"a":"Water Buffalo","b":"1F403","j":["buffalo","water","animal","nature","ox","cow"]},"cow":{"a":"Cow","b":"1F404","j":["beef","ox","animal","nature","moo","milk"]},"pig-face":{"a":"Pig Face","b":"1F437","j":["face","pig","animal","oink","nature"]},"pig":{"a":"Pig","b":"1F416","j":["sow","animal","nature"]},"boar":{"a":"Boar","b":"1F417","j":["pig","animal","nature"]},"pig-nose":{"a":"Pig Nose","b":"1F43D","j":["face","nose","pig","animal","oink"]},"ram":{"a":"Ram","b":"1F40F","j":["Aries","male","sheep","zodiac","animal","nature"]},"ewe":{"a":"Ewe","b":"1F411","j":["female","sheep","animal","nature","wool","shipit"]},"goat":{"a":"Goat","b":"1F410","j":["Capricorn","zodiac","animal","nature"]},"camel":{"a":"Camel","b":"1F42A","j":["dromedary","hump","animal","hot","desert"]},"twohump-camel":{"a":"Two-Hump Camel","b":"1F42B","j":["bactrian","camel","hump","two-hump camel","two_hump_camel","animal","nature","hot","desert"]},"llama":{"a":"Llama","b":"1F999","j":["alpaca","guanaco","vicuña","wool","animal","nature"]},"giraffe":{"a":"Giraffe","b":"1F992","j":["spots","animal","nature","safari"]},"elephant":{"a":"Elephant","b":"1F418","j":["animal","nature","nose","th","circus"]},"mammoth":{"a":"Mammoth","b":"1F9A3","j":["extinction","large","tusk","woolly","elephant","tusks"]},"rhinoceros":{"a":"Rhinoceros","b":"1F98F","j":["animal","nature","horn"]},"hippopotamus":{"a":"Hippopotamus","b":"1F99B","j":["hippo","animal","nature"]},"mouse-face":{"a":"Mouse Face","b":"1F42D","j":["face","mouse","animal","nature","cheese_wedge","rodent"]},"mouse":{"a":"Mouse","b":"1F401","j":["animal","nature","rodent"]},"rat":{"a":"Rat","b":"1F400","j":["animal","mouse","rodent"]},"hamster":{"a":"Hamster","b":"1F439","j":["face","pet","animal","nature"]},"rabbit-face":{"a":"Rabbit Face","b":"1F430","j":["bunny","face","pet","rabbit","animal","nature","spring","magic"]},"rabbit":{"a":"Rabbit","b":"1F407","j":["bunny","pet","animal","nature","magic","spring"]},"chipmunk":{"a":"Chipmunk","b":"1F43F-FE0F","j":["squirrel","animal","nature","rodent"]},"beaver":{"a":"Beaver","b":"1F9AB","j":["dam","animal","rodent"]},"hedgehog":{"a":"Hedgehog","b":"1F994","j":["spiny","animal","nature"]},"bat":{"a":"Bat","b":"1F987","j":["vampire","animal","nature","blind"]},"bear":{"a":"Bear","b":"1F43B","j":["face","animal","nature","wild"]},"polar-bear":{"a":"Polar Bear","b":"1F43B-200D-2744-FE0F","j":["arctic","bear","white","animal"]},"koala":{"a":"Koala","b":"1F428","j":["face","marsupial","animal","nature"]},"panda":{"a":"Panda","b":"1F43C","j":["face","animal","nature"]},"sloth":{"a":"Sloth","b":"1F9A5","j":["lazy","slow","animal"]},"otter":{"a":"Otter","b":"1F9A6","j":["fishing","playful","animal"]},"skunk":{"a":"Skunk","b":"1F9A8","j":["stink","animal"]},"kangaroo":{"a":"Kangaroo","b":"1F998","j":["joey","jump","marsupial","animal","nature","australia","hop"]},"badger":{"a":"Badger","b":"1F9A1","j":["honey badger","pester","animal","nature","honey"]},"paw-prints":{"a":"Paw Prints","b":"1F43E","j":["feet","paw","print","animal","tracking","footprints","dog","cat","pet"]},"turkey":{"a":"Turkey","b":"1F983","j":["bird","animal"]},"chicken":{"a":"Chicken","b":"1F414","j":["bird","animal","cluck","nature"]},"rooster":{"a":"Rooster","b":"1F413","j":["bird","animal","nature","chicken"]},"hatching-chick":{"a":"Hatching Chick","b":"1F423","j":["baby","bird","chick","hatching","animal","chicken","egg","born"]},"baby-chick":{"a":"Baby Chick","b":"1F424","j":["baby","bird","chick","animal","chicken"]},"frontfacing-baby-chick":{"a":"Front-Facing Baby Chick","b":"1F425","j":["baby","bird","chick","front-facing baby chick","front_facing_baby_chick","animal","chicken"]},"bird":{"a":"Bird","b":"1F426-FE0F","j":["animal","nature","fly","tweet","spring"]},"penguin":{"a":"Penguin","b":"1F427","j":["bird","animal","nature"]},"dove":{"a":"Dove","b":"1F54A-FE0F","j":["bird","fly","peace","animal"]},"eagle":{"a":"Eagle","b":"1F985","j":["bird","animal","nature"]},"duck":{"a":"Duck","b":"1F986","j":["bird","animal","nature","mallard"]},"swan":{"a":"Swan","b":"1F9A2","j":["bird","cygnet","ugly duckling","animal","nature"]},"owl":{"a":"Owl","b":"1F989","j":["bird","wise","animal","nature","hoot"]},"dodo":{"a":"Dodo","b":"1F9A4","j":["extinction","large","Mauritius","animal","bird"]},"feather":{"a":"Feather","b":"1FAB6","j":["bird","flight","light","plumage","fly"]},"flamingo":{"a":"Flamingo","b":"1F9A9","j":["flamboyant","tropical","animal"]},"peacock":{"a":"Peacock","b":"1F99A","j":["bird","ostentatious","peahen","proud","animal","nature"]},"parrot":{"a":"Parrot","b":"1F99C","j":["bird","pirate","talk","animal","nature"]},"wing":{"a":"Wing","b":"1FABD","j":["angelic","aviation","bird","flying","mythology","angel","birds"]},"black-bird":{"a":"Black Bird","b":"1F426-200D-2B1B","j":["bird","black","crow","raven","rook"]},"goose":{"a":"Goose","b":"1FABF","j":["bird","fowl","honk","silly","jemima","goosebumps"]},"phoenix":{"a":"⊛ Phoenix","b":"1F426-200D-1F525","j":["fantasy","firebird","phoenix","rebirth","reincarnation"]},"frog":{"a":"Frog","b":"1F438","j":["face","animal","nature","croak","toad"]},"crocodile":{"a":"Crocodile","b":"1F40A","j":["animal","nature","reptile","lizard","alligator"]},"turtle":{"a":"Turtle","b":"1F422","j":["terrapin","tortoise","animal","slow","nature"]},"lizard":{"a":"Lizard","b":"1F98E","j":["reptile","animal","nature"]},"snake":{"a":"Snake","b":"1F40D","j":["bearer","Ophiuchus","serpent","zodiac","animal","evil","nature","hiss","python"]},"dragon-face":{"a":"Dragon Face","b":"1F432","j":["dragon","face","fairy tale","animal","myth","nature","chinese","green"]},"dragon":{"a":"Dragon","b":"1F409","j":["fairy tale","animal","myth","nature","chinese","green"]},"sauropod":{"a":"Sauropod","b":"1F995","j":["brachiosaurus","brontosaurus","diplodocus","animal","nature","dinosaur","extinct"]},"trex":{"a":"T-Rex","b":"1F996","j":["Tyrannosaurus Rex","t_rex","animal","nature","dinosaur","tyrannosaurus","extinct"]},"spouting-whale":{"a":"Spouting Whale","b":"1F433","j":["face","spouting","whale","animal","nature","sea","ocean"]},"whale":{"a":"Whale","b":"1F40B","j":["animal","nature","sea","ocean"]},"dolphin":{"a":"Dolphin","b":"1F42C","j":["flipper","animal","nature","fish","sea","ocean","fins","beach"]},"seal":{"a":"Seal","b":"1F9AD","j":["sea lion","animal","creature","sea"]},"fish":{"a":"Fish","b":"1F41F-FE0F","j":["Pisces","zodiac","animal","food","nature"]},"tropical-fish":{"a":"Tropical Fish","b":"1F420","j":["fish","tropical","animal","swim","ocean","beach","nemo"]},"blowfish":{"a":"Blowfish","b":"1F421","j":["fish","animal","nature","food","sea","ocean"]},"shark":{"a":"Shark","b":"1F988","j":["fish","animal","nature","sea","ocean","jaws","fins","beach"]},"octopus":{"a":"Octopus","b":"1F419","j":["animal","creature","ocean","sea","nature","beach"]},"spiral-shell":{"a":"Spiral Shell","b":"1F41A","j":["shell","spiral","nature","sea","beach"]},"coral":{"a":"Coral","b":"1FAB8","j":["ocean","reef","sea"]},"jellyfish":{"a":"Jellyfish","b":"1FABC","j":["burn","invertebrate","jelly","marine","ouch","stinger","sting","tentacles"]},"snail":{"a":"Snail","b":"1F40C","j":["slow","animal","shell"]},"butterfly":{"a":"Butterfly","b":"1F98B","j":["insect","pretty","animal","nature","caterpillar"]},"bug":{"a":"Bug","b":"1F41B","j":["insect","animal","nature","worm"]},"ant":{"a":"Ant","b":"1F41C","j":["insect","animal","nature","bug"]},"honeybee":{"a":"Honeybee","b":"1F41D","j":["bee","insect","animal","nature","bug","spring","honey"]},"beetle":{"a":"Beetle","b":"1FAB2","j":["bug","insect"]},"lady-beetle":{"a":"Lady Beetle","b":"1F41E","j":["beetle","insect","ladybird","ladybug","animal","nature"]},"cricket":{"a":"Cricket","b":"1F997","j":["grasshopper","Orthoptera","animal","chirp"]},"cockroach":{"a":"Cockroach","b":"1FAB3","j":["insect","pest","roach","pests"]},"spider":{"a":"Spider","b":"1F577-FE0F","j":["insect","animal","arachnid"]},"spider-web":{"a":"Spider Web","b":"1F578-FE0F","j":["spider","web","animal","insect","arachnid","silk"]},"scorpion":{"a":"Scorpion","b":"1F982","j":["scorpio","Scorpio","zodiac","animal","arachnid"]},"mosquito":{"a":"Mosquito","b":"1F99F","j":["disease","fever","malaria","pest","virus","animal","nature","insect"]},"fly":{"a":"Fly","b":"1FAB0","j":["disease","maggot","pest","rotting","insect"]},"worm":{"a":"Worm","b":"1FAB1","j":["annelid","earthworm","parasite","animal"]},"microbe":{"a":"Microbe","b":"1F9A0","j":["amoeba","bacteria","virus","germs","covid"]},"bouquet":{"a":"Bouquet","b":"1F490","j":["flower","flowers","nature","spring"]},"cherry-blossom":{"a":"Cherry Blossom","b":"1F338","j":["blossom","cherry","flower","nature","plant","spring"]},"white-flower":{"a":"White Flower","b":"1F4AE","j":["flower","japanese","spring"]},"lotus":{"a":"Lotus","b":"1FAB7","j":["Buddhism","flower","Hinduism","purity","calm","meditation"]},"rosette":{"a":"Rosette","b":"1F3F5-FE0F","j":["plant","flower","decoration","military"]},"rose":{"a":"Rose","b":"1F339","j":["flower","flowers","valentines","love","spring"]},"wilted-flower":{"a":"Wilted Flower","b":"1F940","j":["flower","wilted","plant","nature","rose"]},"hibiscus":{"a":"Hibiscus","b":"1F33A","j":["flower","plant","vegetable","flowers","beach"]},"sunflower":{"a":"Sunflower","b":"1F33B","j":["flower","sun","nature","plant","fall"]},"blossom":{"a":"Blossom","b":"1F33C","j":["flower","nature","flowers","yellow"]},"tulip":{"a":"Tulip","b":"1F337","j":["flower","flowers","plant","nature","summer","spring"]},"hyacinth":{"a":"Hyacinth","b":"1FABB","j":["bluebonnet","flower","lavender","lupine","snapdragon"]},"seedling":{"a":"Seedling","b":"1F331","j":["young","plant","nature","grass","lawn","spring"]},"potted-plant":{"a":"Potted Plant","b":"1FAB4","j":["boring","grow","house","nurturing","plant","useless","greenery"]},"evergreen-tree":{"a":"Evergreen Tree","b":"1F332","j":["tree","plant","nature"]},"deciduous-tree":{"a":"Deciduous Tree","b":"1F333","j":["deciduous","shedding","tree","plant","nature"]},"palm-tree":{"a":"Palm Tree","b":"1F334","j":["palm","tree","plant","vegetable","nature","summer","beach","mojito","tropical"]},"cactus":{"a":"Cactus","b":"1F335","j":["plant","vegetable","nature"]},"sheaf-of-rice":{"a":"Sheaf of Rice","b":"1F33E","j":["ear","grain","rice","nature","plant"]},"herb":{"a":"Herb","b":"1F33F","j":["leaf","vegetable","plant","medicine","weed","grass","lawn"]},"shamrock":{"a":"Shamrock","b":"2618-FE0F","j":["plant","vegetable","nature","irish","clover"]},"four-leaf-clover":{"a":"Four Leaf Clover","b":"1F340","j":["4","clover","four","four-leaf clover","leaf","vegetable","plant","nature","lucky","irish"]},"maple-leaf":{"a":"Maple Leaf","b":"1F341","j":["falling","leaf","maple","nature","plant","vegetable","ca","fall"]},"fallen-leaf":{"a":"Fallen Leaf","b":"1F342","j":["falling","leaf","nature","plant","vegetable","leaves"]},"leaf-fluttering-in-wind":{"a":"Leaf Fluttering in Wind","b":"1F343","j":["blow","flutter","leaf","wind","nature","plant","tree","vegetable","grass","lawn","spring"]},"empty-nest":{"a":"Empty Nest","b":"1FAB9","j":["nesting","bird"]},"nest-with-eggs":{"a":"Nest with Eggs","b":"1FABA","j":["nesting","bird"]},"mushroom":{"a":"Mushroom","b":"1F344","j":["toadstool","plant","vegetable"]},"grapes":{"a":"Grapes","b":"1F347","j":["fruit","grape","food","wine"]},"melon":{"a":"Melon","b":"1F348","j":["fruit","nature","food"]},"watermelon":{"a":"Watermelon","b":"1F349","j":["fruit","food","picnic","summer"]},"tangerine":{"a":"Tangerine","b":"1F34A","j":["fruit","orange","food","nature"]},"lemon":{"a":"Lemon","b":"1F34B","j":["citrus","fruit","nature"]},"lime":{"a":"⊛ Lime","b":"1F34B-200D-1F7E9","j":["citrus","fruit","lime","tropical"]},"banana":{"a":"Banana","b":"1F34C","j":["fruit","food","monkey"]},"pineapple":{"a":"Pineapple","b":"1F34D","j":["fruit","nature","food"]},"mango":{"a":"Mango","b":"1F96D","j":["fruit","tropical","food"]},"red-apple":{"a":"Red Apple","b":"1F34E","j":["apple","fruit","red","mac","school"]},"green-apple":{"a":"Green Apple","b":"1F34F","j":["apple","fruit","green","nature"]},"pear":{"a":"Pear","b":"1F350","j":["fruit","nature","food"]},"peach":{"a":"Peach","b":"1F351","j":["fruit","nature","food"]},"cherries":{"a":"Cherries","b":"1F352","j":["berries","cherry","fruit","red","food"]},"strawberry":{"a":"Strawberry","b":"1F353","j":["berry","fruit","food","nature"]},"blueberries":{"a":"Blueberries","b":"1FAD0","j":["berry","bilberry","blue","blueberry","fruit"]},"kiwi-fruit":{"a":"Kiwi Fruit","b":"1F95D","j":["food","fruit","kiwi"]},"tomato":{"a":"Tomato","b":"1F345","j":["fruit","vegetable","nature","food"]},"olive":{"a":"Olive","b":"1FAD2","j":["food","fruit"]},"coconut":{"a":"Coconut","b":"1F965","j":["palm","piña colada","fruit","nature","food"]},"avocado":{"a":"Avocado","b":"1F951","j":["food","fruit"]},"eggplant":{"a":"Eggplant","b":"1F346","j":["aubergine","vegetable","nature","food"]},"potato":{"a":"Potato","b":"1F954","j":["food","vegetable","tuber","vegatable","starch"]},"carrot":{"a":"Carrot","b":"1F955","j":["food","vegetable","orange"]},"ear-of-corn":{"a":"Ear of Corn","b":"1F33D","j":["corn","ear","maize","maze","food","vegetable","plant"]},"hot-pepper":{"a":"Hot Pepper","b":"1F336-FE0F","j":["hot","pepper","food","spicy","chilli","chili"]},"bell-pepper":{"a":"Bell Pepper","b":"1FAD1","j":["capsicum","pepper","vegetable","fruit","plant"]},"cucumber":{"a":"Cucumber","b":"1F952","j":["food","pickle","vegetable","fruit"]},"leafy-green":{"a":"Leafy Green","b":"1F96C","j":["bok choy","cabbage","kale","lettuce","food","vegetable","plant"]},"broccoli":{"a":"Broccoli","b":"1F966","j":["wild cabbage","fruit","food","vegetable"]},"garlic":{"a":"Garlic","b":"1F9C4","j":["flavoring","food","spice","cook"]},"onion":{"a":"Onion","b":"1F9C5","j":["flavoring","cook","food","spice"]},"peanuts":{"a":"Peanuts","b":"1F95C","j":["food","nut","peanut","vegetable"]},"beans":{"a":"Beans","b":"1FAD8","j":["food","kidney","legume"]},"chestnut":{"a":"Chestnut","b":"1F330","j":["plant","food","squirrel"]},"ginger-root":{"a":"Ginger Root","b":"1FADA","j":["beer","root","spice","yellow","cooking","gingerbread"]},"pea-pod":{"a":"Pea Pod","b":"1FADB","j":["beans","edamame","legume","pea","pod","vegetable","cozy","green"]},"brown-mushroom":{"a":"⊛ Brown Mushroom","b":"1F344-200D-1F7EB","j":["brown mushroom","food","fungus","nature","vegetable"]},"bread":{"a":"Bread","b":"1F35E","j":["loaf","food","wheat","breakfast","toast"]},"croissant":{"a":"Croissant","b":"1F950","j":["bread","breakfast","food","french","roll"]},"baguette-bread":{"a":"Baguette Bread","b":"1F956","j":["baguette","bread","food","french","france","bakery"]},"flatbread":{"a":"Flatbread","b":"1FAD3","j":["arepa","lavash","naan","pita","flour","food","bakery"]},"pretzel":{"a":"Pretzel","b":"1F968","j":["twisted","convoluted","food","bread","germany","bakery"]},"bagel":{"a":"Bagel","b":"1F96F","j":["bakery","breakfast","schmear","food","bread","jewish_bakery"]},"pancakes":{"a":"Pancakes","b":"1F95E","j":["breakfast","crêpe","food","hotcake","pancake","flapjacks","hotcakes","brunch"]},"waffle":{"a":"Waffle","b":"1F9C7","j":["breakfast","indecisive","iron","food","brunch"]},"cheese-wedge":{"a":"Cheese Wedge","b":"1F9C0","j":["cheese","food","chadder","swiss"]},"meat-on-bone":{"a":"Meat on Bone","b":"1F356","j":["bone","meat","good","food","drumstick"]},"poultry-leg":{"a":"Poultry Leg","b":"1F357","j":["bone","chicken","drumstick","leg","poultry","food","meat","bird","turkey"]},"cut-of-meat":{"a":"Cut of Meat","b":"1F969","j":["chop","lambchop","porkchop","steak","food","cow","meat","cut"]},"bacon":{"a":"Bacon","b":"1F953","j":["breakfast","food","meat","pork","pig","brunch"]},"hamburger":{"a":"Hamburger","b":"1F354","j":["burger","meat","fast food","beef","cheeseburger","mcdonalds","burger king"]},"french-fries":{"a":"French Fries","b":"1F35F","j":["french","fries","chips","snack","fast food","potato"]},"pizza":{"a":"Pizza","b":"1F355","j":["cheese","slice","food","party","italy"]},"hot-dog":{"a":"Hot Dog","b":"1F32D","j":["frankfurter","hotdog","sausage","food","america"]},"sandwich":{"a":"Sandwich","b":"1F96A","j":["bread","food","lunch","toast","bakery"]},"taco":{"a":"Taco","b":"1F32E","j":["mexican","food"]},"burrito":{"a":"Burrito","b":"1F32F","j":["mexican","wrap","food"]},"tamale":{"a":"Tamale","b":"1FAD4","j":["mexican","wrapped","food","masa"]},"stuffed-flatbread":{"a":"Stuffed Flatbread","b":"1F959","j":["falafel","flatbread","food","gyro","kebab","stuffed","mediterranean"]},"falafel":{"a":"Falafel","b":"1F9C6","j":["chickpea","meatball","food","mediterranean"]},"egg":{"a":"Egg","b":"1F95A","j":["breakfast","food","chicken"]},"cooking":{"a":"Cooking","b":"1F373","j":["breakfast","egg","frying","pan","food","kitchen","skillet"]},"shallow-pan-of-food":{"a":"Shallow Pan of Food","b":"1F958","j":["casserole","food","paella","pan","shallow","cooking","skillet"]},"pot-of-food":{"a":"Pot of Food","b":"1F372","j":["pot","stew","food","meat","soup","hot pot"]},"fondue":{"a":"Fondue","b":"1FAD5","j":["cheese","chocolate","melted","pot","Swiss","food"]},"bowl-with-spoon":{"a":"Bowl with Spoon","b":"1F963","j":["breakfast","cereal","congee","oatmeal","porridge","food"]},"green-salad":{"a":"Green Salad","b":"1F957","j":["food","green","salad","healthy","lettuce","vegetable"]},"popcorn":{"a":"Popcorn","b":"1F37F","j":["food","movie theater","films","snack","drama"]},"butter":{"a":"Butter","b":"1F9C8","j":["dairy","food","cook"]},"salt":{"a":"Salt","b":"1F9C2","j":["condiment","shaker"]},"canned-food":{"a":"Canned Food","b":"1F96B","j":["can","food","soup","tomatoes"]},"bento-box":{"a":"Bento Box","b":"1F371","j":["bento","box","food","japanese","lunch"]},"rice-cracker":{"a":"Rice Cracker","b":"1F358","j":["cracker","rice","food","japanese","snack","senbei"]},"rice-ball":{"a":"Rice Ball","b":"1F359","j":["ball","Japanese","rice","food","japanese","onigiri","omusubi"]},"cooked-rice":{"a":"Cooked Rice","b":"1F35A","j":["cooked","rice","food","asian"]},"curry-rice":{"a":"Curry Rice","b":"1F35B","j":["curry","rice","food","spicy","hot","indian"]},"steaming-bowl":{"a":"Steaming Bowl","b":"1F35C","j":["bowl","noodle","ramen","steaming","food","japanese","chopsticks"]},"spaghetti":{"a":"Spaghetti","b":"1F35D","j":["pasta","food","italian","noodle"]},"roasted-sweet-potato":{"a":"Roasted Sweet Potato","b":"1F360","j":["potato","roasted","sweet","food","nature","plant"]},"oden":{"a":"Oden","b":"1F362","j":["kebab","seafood","skewer","stick","food","japanese"]},"sushi":{"a":"Sushi","b":"1F363","j":["food","fish","japanese","rice"]},"fried-shrimp":{"a":"Fried Shrimp","b":"1F364","j":["fried","prawn","shrimp","tempura","food","animal","appetizer","summer"]},"fish-cake-with-swirl":{"a":"Fish Cake with Swirl","b":"1F365","j":["cake","fish","pastry","swirl","food","japan","sea","beach","narutomaki","pink","kamaboko","surimi","ramen"]},"moon-cake":{"a":"Moon Cake","b":"1F96E","j":["autumn","festival","yuèbǐng","food","dessert"]},"dango":{"a":"Dango","b":"1F361","j":["dessert","Japanese","skewer","stick","sweet","food","japanese","barbecue","meat"]},"dumpling":{"a":"Dumpling","b":"1F95F","j":["empanada","gyōza","jiaozi","pierogi","potsticker","food","gyoza"]},"fortune-cookie":{"a":"Fortune Cookie","b":"1F960","j":["prophecy","food","dessert"]},"takeout-box":{"a":"Takeout Box","b":"1F961","j":["oyster pail","food","leftovers"]},"crab":{"a":"Crab","b":"1F980","j":["Cancer","zodiac","animal","crustacean"]},"lobster":{"a":"Lobster","b":"1F99E","j":["bisque","claws","seafood","animal","nature"]},"shrimp":{"a":"Shrimp","b":"1F990","j":["food","shellfish","small","animal","ocean","nature","seafood"]},"squid":{"a":"Squid","b":"1F991","j":["food","molusc","animal","nature","ocean","sea"]},"oyster":{"a":"Oyster","b":"1F9AA","j":["diving","pearl","food"]},"soft-ice-cream":{"a":"Soft Ice Cream","b":"1F366","j":["cream","dessert","ice","icecream","soft","sweet","food","hot","summer"]},"shaved-ice":{"a":"Shaved Ice","b":"1F367","j":["dessert","ice","shaved","sweet","hot","summer"]},"ice-cream":{"a":"Ice Cream","b":"1F368","j":["cream","dessert","ice","sweet","food","hot"]},"doughnut":{"a":"Doughnut","b":"1F369","j":["breakfast","dessert","donut","sweet","food","snack"]},"cookie":{"a":"Cookie","b":"1F36A","j":["dessert","sweet","food","snack","oreo","chocolate"]},"birthday-cake":{"a":"Birthday Cake","b":"1F382","j":["birthday","cake","celebration","dessert","pastry","sweet","food"]},"shortcake":{"a":"Shortcake","b":"1F370","j":["cake","dessert","pastry","slice","sweet","food"]},"cupcake":{"a":"Cupcake","b":"1F9C1","j":["bakery","sweet","food","dessert"]},"pie":{"a":"Pie","b":"1F967","j":["filling","pastry","fruit","meat","food","dessert"]},"chocolate-bar":{"a":"Chocolate Bar","b":"1F36B","j":["bar","chocolate","dessert","sweet","food","snack"]},"candy":{"a":"Candy","b":"1F36C","j":["dessert","sweet","snack","lolly"]},"lollipop":{"a":"Lollipop","b":"1F36D","j":["candy","dessert","sweet","food","snack"]},"custard":{"a":"Custard","b":"1F36E","j":["dessert","pudding","sweet","food","flan"]},"honey-pot":{"a":"Honey Pot","b":"1F36F","j":["honey","honeypot","pot","sweet","bees","kitchen"]},"baby-bottle":{"a":"Baby Bottle","b":"1F37C","j":["baby","bottle","drink","milk","food","container"]},"glass-of-milk":{"a":"Glass of Milk","b":"1F95B","j":["drink","glass","milk","beverage","cow"]},"hot-beverage":{"a":"Hot Beverage","b":"2615-FE0F","j":["beverage","coffee","drink","hot","steaming","tea","caffeine","latte","espresso","mug"]},"teapot":{"a":"Teapot","b":"1FAD6","j":["drink","pot","tea","hot"]},"teacup-without-handle":{"a":"Teacup Without Handle","b":"1F375","j":["beverage","cup","drink","tea","teacup","bowl","breakfast","green","british"]},"sake":{"a":"Sake","b":"1F376","j":["bar","beverage","bottle","cup","drink","wine","drunk","japanese","alcohol","booze"]},"bottle-with-popping-cork":{"a":"Bottle with Popping Cork","b":"1F37E","j":["bar","bottle","cork","drink","popping","wine","celebration"]},"wine-glass":{"a":"Wine Glass","b":"1F377","j":["bar","beverage","drink","glass","wine","drunk","alcohol","booze"]},"cocktail-glass":{"a":"Cocktail Glass","b":"1F378-FE0F","j":["bar","cocktail","drink","glass","drunk","alcohol","beverage","booze","mojito"]},"tropical-drink":{"a":"Tropical Drink","b":"1F379","j":["bar","drink","tropical","beverage","cocktail","summer","beach","alcohol","booze","mojito"]},"beer-mug":{"a":"Beer Mug","b":"1F37A","j":["bar","beer","drink","mug","relax","beverage","drunk","party","pub","summer","alcohol","booze"]},"clinking-beer-mugs":{"a":"Clinking Beer Mugs","b":"1F37B","j":["bar","beer","clink","drink","mug","relax","beverage","drunk","party","pub","summer","alcohol","booze"]},"clinking-glasses":{"a":"Clinking Glasses","b":"1F942","j":["celebrate","clink","drink","glass","beverage","party","alcohol","cheers","wine","champagne","toast"]},"tumbler-glass":{"a":"Tumbler Glass","b":"1F943","j":["glass","liquor","shot","tumbler","whisky","drink","beverage","drunk","alcohol","booze","bourbon","scotch"]},"pouring-liquid":{"a":"Pouring Liquid","b":"1FAD7","j":["drink","empty","glass","spill","cup","water"]},"cup-with-straw":{"a":"Cup with Straw","b":"1F964","j":["juice","soda","malt","soft drink","water","drink"]},"bubble-tea":{"a":"Bubble Tea","b":"1F9CB","j":["bubble","milk","pearl","tea","taiwan","boba","milk tea","straw"]},"beverage-box":{"a":"Beverage Box","b":"1F9C3","j":["beverage","box","juice","straw","sweet","drink"]},"mate":{"a":"Mate","b":"1F9C9","j":["drink","tea","beverage"]},"ice":{"a":"Ice","b":"1F9CA","j":["cold","ice cube","iceberg","water"]},"chopsticks":{"a":"Chopsticks","b":"1F962","j":["hashi","jeotgarak","kuaizi","food"]},"fork-and-knife-with-plate":{"a":"Fork and Knife with Plate","b":"1F37D-FE0F","j":["cooking","fork","knife","plate","food","eat","meal","lunch","dinner","restaurant"]},"fork-and-knife":{"a":"Fork and Knife","b":"1F374","j":["cooking","cutlery","fork","knife","kitchen"]},"spoon":{"a":"Spoon","b":"1F944","j":["tableware","cutlery","kitchen"]},"kitchen-knife":{"a":"Kitchen Knife","b":"1F52A","j":["cooking","hocho","knife","tool","weapon","blade","cutlery","kitchen"]},"jar":{"a":"Jar","b":"1FAD9","j":["condiment","container","empty","sauce","store"]},"amphora":{"a":"Amphora","b":"1F3FA","j":["Aquarius","cooking","drink","jug","zodiac","vase","jar"]},"globe-showing-europeafrica":{"a":"Globe Showing Europe-Africa","b":"1F30D-FE0F","j":["Africa","earth","Europe","globe","globe showing Europe-Africa","world","globe_showing_europe_africa","international"]},"globe-showing-americas":{"a":"Globe Showing Americas","b":"1F30E-FE0F","j":["Americas","earth","globe","globe showing Americas","world","USA","international"]},"globe-showing-asiaaustralia":{"a":"Globe Showing Asia-Australia","b":"1F30F-FE0F","j":["Asia","Australia","earth","globe","globe showing Asia-Australia","world","globe_showing_asia_australia","east","international"]},"globe-with-meridians":{"a":"Globe with Meridians","b":"1F310","j":["earth","globe","meridians","world","international","internet","interweb","i18n"]},"world-map":{"a":"World Map","b":"1F5FA-FE0F","j":["map","world","location","direction"]},"map-of-japan":{"a":"Map of Japan","b":"1F5FE","j":["Japan","map","map of Japan","nation","country","japanese","asia"]},"compass":{"a":"Compass","b":"1F9ED","j":["magnetic","navigation","orienteering"]},"snowcapped-mountain":{"a":"Snow-Capped Mountain","b":"1F3D4-FE0F","j":["cold","mountain","snow","snow-capped mountain","snow_capped_mountain","photo","nature","environment","winter"]},"mountain":{"a":"Mountain","b":"26F0-FE0F","j":["photo","nature","environment"]},"volcano":{"a":"Volcano","b":"1F30B","j":["eruption","mountain","photo","nature","disaster"]},"mount-fuji":{"a":"Mount Fuji","b":"1F5FB","j":["fuji","mountain","photo","nature","japanese"]},"camping":{"a":"Camping","b":"1F3D5-FE0F","j":["photo","outdoors","tent"]},"beach-with-umbrella":{"a":"Beach with Umbrella","b":"1F3D6-FE0F","j":["beach","umbrella","weather","summer","sunny","sand","mojito"]},"desert":{"a":"Desert","b":"1F3DC-FE0F","j":["photo","warm","saharah"]},"desert-island":{"a":"Desert Island","b":"1F3DD-FE0F","j":["desert","island","photo","tropical","mojito"]},"national-park":{"a":"National Park","b":"1F3DE-FE0F","j":["park","photo","environment","nature"]},"stadium":{"a":"Stadium","b":"1F3DF-FE0F","j":["photo","place","sports","concert","venue"]},"classical-building":{"a":"Classical Building","b":"1F3DB-FE0F","j":["classical","art","culture","history"]},"building-construction":{"a":"Building Construction","b":"1F3D7-FE0F","j":["construction","wip","working","progress"]},"brick":{"a":"Brick","b":"1F9F1","j":["bricks","clay","mortar","wall"]},"rock":{"a":"Rock","b":"1FAA8","j":["boulder","heavy","solid","stone"]},"wood":{"a":"Wood","b":"1FAB5","j":["log","lumber","timber","nature","trunk"]},"hut":{"a":"Hut","b":"1F6D6","j":["house","roundhouse","yurt","structure"]},"houses":{"a":"Houses","b":"1F3D8-FE0F","j":["buildings","photo"]},"derelict-house":{"a":"Derelict House","b":"1F3DA-FE0F","j":["derelict","house","abandon","evict","broken","building"]},"house":{"a":"House","b":"1F3E0-FE0F","j":["home","building"]},"house-with-garden":{"a":"House with Garden","b":"1F3E1","j":["garden","home","house","plant","nature"]},"office-building":{"a":"Office Building","b":"1F3E2","j":["building","bureau","work"]},"japanese-post-office":{"a":"Japanese Post Office","b":"1F3E3","j":["Japanese","Japanese post office","post","building","envelope","communication"]},"post-office":{"a":"Post Office","b":"1F3E4","j":["European","post","building","email"]},"hospital":{"a":"Hospital","b":"1F3E5","j":["doctor","medicine","building","health","surgery"]},"bank":{"a":"Bank","b":"1F3E6","j":["building","money","sales","cash","business","enterprise"]},"hotel":{"a":"Hotel","b":"1F3E8","j":["building","accomodation","checkin"]},"love-hotel":{"a":"Love Hotel","b":"1F3E9","j":["hotel","love","like","affection","dating"]},"convenience-store":{"a":"Convenience Store","b":"1F3EA","j":["convenience","store","building","shopping","groceries"]},"school":{"a":"School","b":"1F3EB","j":["building","student","education","learn","teach"]},"department-store":{"a":"Department Store","b":"1F3EC","j":["department","store","building","shopping","mall"]},"factory":{"a":"Factory","b":"1F3ED-FE0F","j":["building","industry","pollution","smoke"]},"japanese-castle":{"a":"Japanese Castle","b":"1F3EF","j":["castle","Japanese","photo","building"]},"castle":{"a":"Castle","b":"1F3F0","j":["European","building","royalty","history"]},"wedding":{"a":"Wedding","b":"1F492","j":["chapel","romance","love","like","affection","couple","marriage","bride","groom"]},"tokyo-tower":{"a":"Tokyo Tower","b":"1F5FC","j":["Tokyo","tower","photo","japanese"]},"statue-of-liberty":{"a":"Statue of Liberty","b":"1F5FD","j":["liberty","statue","american","newyork"]},"church":{"a":"Church","b":"26EA-FE0F","j":["Christian","cross","religion","building","christ"]},"mosque":{"a":"Mosque","b":"1F54C","j":["islam","Muslim","religion","worship","minaret"]},"hindu-temple":{"a":"Hindu Temple","b":"1F6D5","j":["hindu","temple","religion"]},"synagogue":{"a":"Synagogue","b":"1F54D","j":["Jew","Jewish","religion","temple","judaism","worship","jewish"]},"shinto-shrine":{"a":"Shinto Shrine","b":"26E9-FE0F","j":["religion","shinto","shrine","temple","japan","kyoto"]},"kaaba":{"a":"Kaaba","b":"1F54B","j":["islam","Muslim","religion","mecca","mosque"]},"fountain":{"a":"Fountain","b":"26F2-FE0F","j":["photo","summer","water","fresh"]},"tent":{"a":"Tent","b":"26FA-FE0F","j":["camping","photo","outdoors"]},"foggy":{"a":"Foggy","b":"1F301","j":["fog","photo","mountain"]},"night-with-stars":{"a":"Night with Stars","b":"1F303","j":["night","star","evening","city","downtown"]},"cityscape":{"a":"Cityscape","b":"1F3D9-FE0F","j":["city","photo","night life","urban"]},"sunrise-over-mountains":{"a":"Sunrise over Mountains","b":"1F304","j":["morning","mountain","sun","sunrise","view","vacation","photo"]},"sunrise":{"a":"Sunrise","b":"1F305","j":["morning","sun","view","vacation","photo"]},"cityscape-at-dusk":{"a":"Cityscape at Dusk","b":"1F306","j":["city","dusk","evening","landscape","sunset","photo","sky","buildings"]},"sunset":{"a":"Sunset","b":"1F307","j":["dusk","sun","photo","good morning","dawn"]},"bridge-at-night":{"a":"Bridge at Night","b":"1F309","j":["bridge","night","photo","sanfrancisco"]},"hot-springs":{"a":"Hot Springs","b":"2668-FE0F","j":["hot","hotsprings","springs","steaming","bath","warm","relax"]},"carousel-horse":{"a":"Carousel Horse","b":"1F3A0","j":["carousel","horse","photo","carnival"]},"playground-slide":{"a":"Playground Slide","b":"1F6DD","j":["amusement park","play","theme park","fun","park"]},"ferris-wheel":{"a":"Ferris Wheel","b":"1F3A1","j":["amusement park","ferris","theme park","wheel","photo","carnival","londoneye"]},"roller-coaster":{"a":"Roller Coaster","b":"1F3A2","j":["amusement park","coaster","roller","theme park","carnival","playground","photo","fun"]},"barber-pole":{"a":"Barber Pole","b":"1F488","j":["barber","haircut","pole","hair","salon","style"]},"circus-tent":{"a":"Circus Tent","b":"1F3AA","j":["circus","tent","festival","carnival","party"]},"locomotive":{"a":"Locomotive","b":"1F682","j":["engine","railway","steam","train","transportation","vehicle"]},"railway-car":{"a":"Railway Car","b":"1F683","j":["car","electric","railway","train","tram","trolleybus","transportation","vehicle"]},"highspeed-train":{"a":"High-Speed Train","b":"1F684","j":["high-speed train","railway","shinkansen","speed","train","high_speed_train","transportation","vehicle"]},"bullet-train":{"a":"Bullet Train","b":"1F685","j":["bullet","railway","shinkansen","speed","train","transportation","vehicle","fast","public","travel"]},"train":{"a":"Train","b":"1F686","j":["railway","transportation","vehicle"]},"metro":{"a":"Metro","b":"1F687-FE0F","j":["subway","transportation","blue-square","mrt","underground","tube"]},"light-rail":{"a":"Light Rail","b":"1F688","j":["railway","transportation","vehicle"]},"station":{"a":"Station","b":"1F689","j":["railway","train","transportation","vehicle","public"]},"tram":{"a":"Tram","b":"1F68A","j":["trolleybus","transportation","vehicle"]},"monorail":{"a":"Monorail","b":"1F69D","j":["vehicle","transportation"]},"mountain-railway":{"a":"Mountain Railway","b":"1F69E","j":["car","mountain","railway","transportation","vehicle"]},"tram-car":{"a":"Tram Car","b":"1F68B","j":["car","tram","trolleybus","transportation","vehicle","carriage","public","travel"]},"bus":{"a":"Bus","b":"1F68C","j":["vehicle","car","transportation"]},"oncoming-bus":{"a":"Oncoming Bus","b":"1F68D-FE0F","j":["bus","oncoming","vehicle","transportation"]},"trolleybus":{"a":"Trolleybus","b":"1F68E","j":["bus","tram","trolley","bart","transportation","vehicle"]},"minibus":{"a":"Minibus","b":"1F690","j":["bus","vehicle","car","transportation"]},"ambulance":{"a":"Ambulance","b":"1F691-FE0F","j":["vehicle","health","911","hospital"]},"fire-engine":{"a":"Fire Engine","b":"1F692","j":["engine","fire","truck","transportation","cars","vehicle"]},"police-car":{"a":"Police Car","b":"1F693","j":["car","patrol","police","vehicle","cars","transportation","law","legal","enforcement"]},"oncoming-police-car":{"a":"Oncoming Police Car","b":"1F694-FE0F","j":["car","oncoming","police","vehicle","law","legal","enforcement","911"]},"taxi":{"a":"Taxi","b":"1F695","j":["vehicle","uber","cars","transportation"]},"oncoming-taxi":{"a":"Oncoming Taxi","b":"1F696","j":["oncoming","taxi","vehicle","cars","uber"]},"automobile":{"a":"Automobile","b":"1F697","j":["car","red","transportation","vehicle"]},"oncoming-automobile":{"a":"Oncoming Automobile","b":"1F698-FE0F","j":["automobile","car","oncoming","vehicle","transportation"]},"sport-utility-vehicle":{"a":"Sport Utility Vehicle","b":"1F699","j":["recreational","sport utility","transportation","vehicle"]},"pickup-truck":{"a":"Pickup Truck","b":"1F6FB","j":["pick-up","pickup","truck","car","transportation"]},"delivery-truck":{"a":"Delivery Truck","b":"1F69A","j":["delivery","truck","cars","transportation"]},"articulated-lorry":{"a":"Articulated Lorry","b":"1F69B","j":["lorry","semi","truck","vehicle","cars","transportation","express"]},"tractor":{"a":"Tractor","b":"1F69C","j":["vehicle","car","farming","agriculture"]},"racing-car":{"a":"Racing Car","b":"1F3CE-FE0F","j":["car","racing","sports","race","fast","formula","f1"]},"motorcycle":{"a":"Motorcycle","b":"1F3CD-FE0F","j":["racing","race","sports","fast"]},"motor-scooter":{"a":"Motor Scooter","b":"1F6F5","j":["motor","scooter","vehicle","vespa","sasha"]},"manual-wheelchair":{"a":"Manual Wheelchair","b":"1F9BD","j":["accessibility"]},"motorized-wheelchair":{"a":"Motorized Wheelchair","b":"1F9BC","j":["accessibility"]},"auto-rickshaw":{"a":"Auto Rickshaw","b":"1F6FA","j":["tuk tuk","move","transportation"]},"bicycle":{"a":"Bicycle","b":"1F6B2-FE0F","j":["bike","sports","exercise","hipster"]},"kick-scooter":{"a":"Kick Scooter","b":"1F6F4","j":["kick","scooter","vehicle","razor"]},"skateboard":{"a":"Skateboard","b":"1F6F9","j":["board"]},"roller-skate":{"a":"Roller Skate","b":"1F6FC","j":["roller","skate","footwear","sports"]},"bus-stop":{"a":"Bus Stop","b":"1F68F","j":["bus","stop","transportation","wait"]},"motorway":{"a":"Motorway","b":"1F6E3-FE0F","j":["highway","road","cupertino","interstate"]},"railway-track":{"a":"Railway Track","b":"1F6E4-FE0F","j":["railway","train","transportation"]},"oil-drum":{"a":"Oil Drum","b":"1F6E2-FE0F","j":["drum","oil","barrell"]},"fuel-pump":{"a":"Fuel Pump","b":"26FD-FE0F","j":["diesel","fuel","fuelpump","gas","pump","station","gas station","petroleum"]},"wheel":{"a":"Wheel","b":"1F6DE","j":["circle","tire","turn","car","transport"]},"police-car-light":{"a":"Police Car Light","b":"1F6A8","j":["beacon","car","light","police","revolving","ambulance","911","emergency","alert","error","pinged","law","legal"]},"horizontal-traffic-light":{"a":"Horizontal Traffic Light","b":"1F6A5","j":["light","signal","traffic","transportation"]},"vertical-traffic-light":{"a":"Vertical Traffic Light","b":"1F6A6","j":["light","signal","traffic","transportation","driving"]},"stop-sign":{"a":"Stop Sign","b":"1F6D1","j":["octagonal","sign","stop"]},"construction":{"a":"Construction","b":"1F6A7","j":["barrier","wip","progress","caution","warning"]},"anchor":{"a":"Anchor","b":"2693-FE0F","j":["ship","tool","ferry","sea","boat"]},"ring-buoy":{"a":"Ring Buoy","b":"1F6DF","j":["float","life preserver","life saver","rescue","safety"]},"sailboat":{"a":"Sailboat","b":"26F5-FE0F","j":["boat","resort","sea","yacht","ship","summer","transportation","water","sailing"]},"canoe":{"a":"Canoe","b":"1F6F6","j":["boat","paddle","water","ship"]},"speedboat":{"a":"Speedboat","b":"1F6A4","j":["boat","ship","transportation","vehicle","summer"]},"passenger-ship":{"a":"Passenger Ship","b":"1F6F3-FE0F","j":["passenger","ship","yacht","cruise","ferry"]},"ferry":{"a":"Ferry","b":"26F4-FE0F","j":["boat","passenger","ship","yacht"]},"motor-boat":{"a":"Motor Boat","b":"1F6E5-FE0F","j":["boat","motorboat","ship"]},"ship":{"a":"Ship","b":"1F6A2","j":["boat","passenger","transportation","titanic","deploy"]},"airplane":{"a":"Airplane","b":"2708-FE0F","j":["aeroplane","vehicle","transportation","flight","fly"]},"small-airplane":{"a":"Small Airplane","b":"1F6E9-FE0F","j":["aeroplane","airplane","flight","transportation","fly","vehicle"]},"airplane-departure":{"a":"Airplane Departure","b":"1F6EB","j":["aeroplane","airplane","check-in","departure","departures","airport","flight","landing"]},"airplane-arrival":{"a":"Airplane Arrival","b":"1F6EC","j":["aeroplane","airplane","arrivals","arriving","landing","airport","flight","boarding"]},"parachute":{"a":"Parachute","b":"1FA82","j":["hang-glide","parasail","skydive","fly","glide"]},"seat":{"a":"Seat","b":"1F4BA","j":["chair","sit","airplane","transport","bus","flight","fly"]},"helicopter":{"a":"Helicopter","b":"1F681","j":["vehicle","transportation","fly"]},"suspension-railway":{"a":"Suspension Railway","b":"1F69F","j":["railway","suspension","vehicle","transportation"]},"mountain-cableway":{"a":"Mountain Cableway","b":"1F6A0","j":["cable","gondola","mountain","transportation","vehicle","ski"]},"aerial-tramway":{"a":"Aerial Tramway","b":"1F6A1","j":["aerial","cable","car","gondola","tramway","transportation","vehicle","ski"]},"satellite":{"a":"Satellite","b":"1F6F0-FE0F","j":["space","communication","gps","orbit","spaceflight","NASA","ISS"]},"rocket":{"a":"Rocket","b":"1F680","j":["space","launch","ship","staffmode","NASA","outer space","outer_space","fly"]},"flying-saucer":{"a":"Flying Saucer","b":"1F6F8","j":["UFO","transportation","vehicle","ufo"]},"bellhop-bell":{"a":"Bellhop Bell","b":"1F6CE-FE0F","j":["bell","bellhop","hotel","service"]},"luggage":{"a":"Luggage","b":"1F9F3","j":["packing","travel"]},"hourglass-done":{"a":"Hourglass Done","b":"231B-FE0F","j":["sand","timer","time","clock","oldschool","limit","exam","quiz","test"]},"hourglass-not-done":{"a":"Hourglass Not Done","b":"23F3-FE0F","j":["hourglass","sand","timer","oldschool","time","countdown"]},"watch":{"a":"Watch","b":"231A-FE0F","j":["clock","time","accessories"]},"alarm-clock":{"a":"Alarm Clock","b":"23F0","j":["alarm","clock","time","wake"]},"stopwatch":{"a":"Stopwatch","b":"23F1-FE0F","j":["clock","time","deadline"]},"timer-clock":{"a":"Timer Clock","b":"23F2-FE0F","j":["clock","timer","alarm"]},"mantelpiece-clock":{"a":"Mantelpiece Clock","b":"1F570-FE0F","j":["clock","time"]},"twelve-oclock":{"a":"Twelve O’Clock","b":"1F55B-FE0F","j":["00","12","12:00","clock","o’clock","twelve","twelve_o_clock","00:00","0000","1200","time","noon","midnight","midday","late","early","schedule"]},"twelvethirty":{"a":"Twelve-Thirty","b":"1F567-FE0F","j":["12","12:30","clock","thirty","twelve","twelve-thirty","twelve_thirty","00:30","0030","1230","time","late","early","schedule"]},"one-oclock":{"a":"One O’Clock","b":"1F550-FE0F","j":["00","1","1:00","clock","o’clock","one","one_o_clock","100","13:00","1300","time","late","early","schedule"]},"onethirty":{"a":"One-Thirty","b":"1F55C-FE0F","j":["1","1:30","clock","one","one-thirty","thirty","one_thirty","130","13:30","1330","time","late","early","schedule"]},"two-oclock":{"a":"Two O’Clock","b":"1F551-FE0F","j":["00","2","2:00","clock","o’clock","two","two_o_clock","200","14:00","1400","time","late","early","schedule"]},"twothirty":{"a":"Two-Thirty","b":"1F55D-FE0F","j":["2","2:30","clock","thirty","two","two-thirty","two_thirty","230","14:30","1430","time","late","early","schedule"]},"three-oclock":{"a":"Three O’Clock","b":"1F552-FE0F","j":["00","3","3:00","clock","o’clock","three","three_o_clock","300","15:00","1500","time","late","early","schedule"]},"threethirty":{"a":"Three-Thirty","b":"1F55E-FE0F","j":["3","3:30","clock","thirty","three","three-thirty","three_thirty","330","15:30","1530","time","late","early","schedule"]},"four-oclock":{"a":"Four O’Clock","b":"1F553-FE0F","j":["00","4","4:00","clock","four","o’clock","four_o_clock","400","16:00","1600","time","late","early","schedule"]},"fourthirty":{"a":"Four-Thirty","b":"1F55F-FE0F","j":["4","4:30","clock","four","four-thirty","thirty","four_thirty","430","16:30","1630","time","late","early","schedule"]},"five-oclock":{"a":"Five O’Clock","b":"1F554-FE0F","j":["00","5","5:00","clock","five","o’clock","five_o_clock","500","17:00","1700","time","late","early","schedule"]},"fivethirty":{"a":"Five-Thirty","b":"1F560-FE0F","j":["5","5:30","clock","five","five-thirty","thirty","five_thirty","530","17:30","1730","time","late","early","schedule"]},"six-oclock":{"a":"Six O’Clock","b":"1F555-FE0F","j":["00","6","6:00","clock","o’clock","six","six_o_clock","600","18:00","1800","time","late","early","schedule","dawn","dusk"]},"sixthirty":{"a":"Six-Thirty","b":"1F561-FE0F","j":["6","6:30","clock","six","six-thirty","thirty","six_thirty","630","18:30","1830","time","late","early","schedule"]},"seven-oclock":{"a":"Seven O’Clock","b":"1F556-FE0F","j":["00","7","7:00","clock","o’clock","seven","seven_o_clock","700","19:00","1900","time","late","early","schedule"]},"seventhirty":{"a":"Seven-Thirty","b":"1F562-FE0F","j":["7","7:30","clock","seven","seven-thirty","thirty","seven_thirty","730","19:30","1930","time","late","early","schedule"]},"eight-oclock":{"a":"Eight O’Clock","b":"1F557-FE0F","j":["00","8","8:00","clock","eight","o’clock","eight_o_clock","800","20:00","2000","time","late","early","schedule"]},"eightthirty":{"a":"Eight-Thirty","b":"1F563-FE0F","j":["8","8:30","clock","eight","eight-thirty","thirty","eight_thirty","830","20:30","2030","time","late","early","schedule"]},"nine-oclock":{"a":"Nine O’Clock","b":"1F558-FE0F","j":["00","9","9:00","clock","nine","o’clock","nine_o_clock","900","21:00","2100","time","late","early","schedule"]},"ninethirty":{"a":"Nine-Thirty","b":"1F564-FE0F","j":["9","9:30","clock","nine","nine-thirty","thirty","nine_thirty","930","21:30","2130","time","late","early","schedule"]},"ten-oclock":{"a":"Ten O’Clock","b":"1F559-FE0F","j":["00","10","10:00","clock","o’clock","ten","ten_o_clock","1000","22:00","2200","time","late","early","schedule"]},"tenthirty":{"a":"Ten-Thirty","b":"1F565-FE0F","j":["10","10:30","clock","ten","ten-thirty","thirty","ten_thirty","1030","22:30","2230","time","late","early","schedule"]},"eleven-oclock":{"a":"Eleven O’Clock","b":"1F55A-FE0F","j":["00","11","11:00","clock","eleven","o’clock","eleven_o_clock","1100","23:00","2300","time","late","early","schedule"]},"eleventhirty":{"a":"Eleven-Thirty","b":"1F566-FE0F","j":["11","11:30","clock","eleven","eleven-thirty","thirty","eleven_thirty","1130","23:30","2330","time","late","early","schedule"]},"new-moon":{"a":"New Moon","b":"1F311","j":["dark","moon","nature","twilight","planet","space","night","evening","sleep"]},"waxing-crescent-moon":{"a":"Waxing Crescent Moon","b":"1F312","j":["crescent","moon","waxing","nature","twilight","planet","space","night","evening","sleep"]},"first-quarter-moon":{"a":"First Quarter Moon","b":"1F313","j":["moon","quarter","nature","twilight","planet","space","night","evening","sleep"]},"waxing-gibbous-moon":{"a":"Waxing Gibbous Moon","b":"1F314","j":["gibbous","moon","waxing","nature","night","sky","gray","twilight","planet","space","evening","sleep"]},"full-moon":{"a":"Full Moon","b":"1F315-FE0F","j":["full","moon","nature","yellow","twilight","planet","space","night","evening","sleep"]},"waning-gibbous-moon":{"a":"Waning Gibbous Moon","b":"1F316","j":["gibbous","moon","waning","nature","twilight","planet","space","night","evening","sleep","waxing_gibbous_moon"]},"last-quarter-moon":{"a":"Last Quarter Moon","b":"1F317","j":["moon","quarter","nature","twilight","planet","space","night","evening","sleep"]},"waning-crescent-moon":{"a":"Waning Crescent Moon","b":"1F318","j":["crescent","moon","waning","nature","twilight","planet","space","night","evening","sleep"]},"crescent-moon":{"a":"Crescent Moon","b":"1F319","j":["crescent","moon","night","sleep","sky","evening","magic"]},"new-moon-face":{"a":"New Moon Face","b":"1F31A","j":["face","moon","nature","twilight","planet","space","night","evening","sleep"]},"first-quarter-moon-face":{"a":"First Quarter Moon Face","b":"1F31B","j":["face","moon","quarter","nature","twilight","planet","space","night","evening","sleep"]},"last-quarter-moon-face":{"a":"Last Quarter Moon Face","b":"1F31C-FE0F","j":["face","moon","quarter","nature","twilight","planet","space","night","evening","sleep"]},"thermometer":{"a":"Thermometer","b":"1F321-FE0F","j":["weather","temperature","hot","cold"]},"sun":{"a":"Sun","b":"2600-FE0F","j":["bright","rays","sunny","weather","nature","brightness","summer","beach","spring"]},"full-moon-face":{"a":"Full Moon Face","b":"1F31D","j":["bright","face","full","moon","nature","twilight","planet","space","night","evening","sleep"]},"sun-with-face":{"a":"Sun with Face","b":"1F31E","j":["bright","face","sun","nature","morning","sky"]},"ringed-planet":{"a":"Ringed Planet","b":"1FA90","j":["saturn","saturnine","outerspace"]},"star":{"a":"Star","b":"2B50-FE0F","j":["night","yellow"]},"glowing-star":{"a":"Glowing Star","b":"1F31F","j":["glittery","glow","shining","sparkle","star","night","awesome","good","magic"]},"shooting-star":{"a":"Shooting Star","b":"1F320","j":["falling","shooting","star","night","photo"]},"milky-way":{"a":"Milky Way","b":"1F30C","j":["space","photo","stars"]},"cloud":{"a":"Cloud","b":"2601-FE0F","j":["weather","sky"]},"sun-behind-cloud":{"a":"Sun Behind Cloud","b":"26C5-FE0F","j":["cloud","sun","weather","nature","cloudy","morning","fall","spring"]},"cloud-with-lightning-and-rain":{"a":"Cloud with Lightning and Rain","b":"26C8-FE0F","j":["cloud","rain","thunder","weather","lightning"]},"sun-behind-small-cloud":{"a":"Sun Behind Small Cloud","b":"1F324-FE0F","j":["cloud","sun","weather"]},"sun-behind-large-cloud":{"a":"Sun Behind Large Cloud","b":"1F325-FE0F","j":["cloud","sun","weather"]},"sun-behind-rain-cloud":{"a":"Sun Behind Rain Cloud","b":"1F326-FE0F","j":["cloud","rain","sun","weather"]},"cloud-with-rain":{"a":"Cloud with Rain","b":"1F327-FE0F","j":["cloud","rain","weather"]},"cloud-with-snow":{"a":"Cloud with Snow","b":"1F328-FE0F","j":["cloud","cold","snow","weather"]},"cloud-with-lightning":{"a":"Cloud with Lightning","b":"1F329-FE0F","j":["cloud","lightning","weather","thunder"]},"tornado":{"a":"Tornado","b":"1F32A-FE0F","j":["cloud","whirlwind","weather","cyclone","twister"]},"fog":{"a":"Fog","b":"1F32B-FE0F","j":["cloud","weather"]},"wind-face":{"a":"Wind Face","b":"1F32C-FE0F","j":["blow","cloud","face","wind","gust","air"]},"cyclone":{"a":"Cyclone","b":"1F300","j":["dizzy","hurricane","twister","typhoon","weather","swirl","blue","cloud","vortex","spiral","whirlpool","spin","tornado"]},"rainbow":{"a":"Rainbow","b":"1F308","j":["rain","nature","happy","unicorn_face","photo","sky","spring"]},"closed-umbrella":{"a":"Closed Umbrella","b":"1F302","j":["clothing","rain","umbrella","weather","drizzle"]},"umbrella":{"a":"Umbrella","b":"2602-FE0F","j":["clothing","rain","weather","spring"]},"umbrella-with-rain-drops":{"a":"Umbrella with Rain Drops","b":"2614-FE0F","j":["clothing","drop","rain","umbrella","rainy","weather","spring"]},"umbrella-on-ground":{"a":"Umbrella on Ground","b":"26F1-FE0F","j":["rain","sun","umbrella","weather","summer"]},"high-voltage":{"a":"High Voltage","b":"26A1-FE0F","j":["danger","electric","lightning","voltage","zap","thunder","weather","lightning bolt","fast"]},"snowflake":{"a":"Snowflake","b":"2744-FE0F","j":["cold","snow","winter","season","weather","christmas","xmas"]},"snowman":{"a":"Snowman","b":"2603-FE0F","j":["cold","snow","winter","season","weather","christmas","xmas","frozen"]},"snowman-without-snow":{"a":"Snowman Without Snow","b":"26C4-FE0F","j":["cold","snow","snowman","winter","season","weather","christmas","xmas","frozen","without_snow"]},"comet":{"a":"Comet","b":"2604-FE0F","j":["space"]},"fire":{"a":"Fire","b":"1F525","j":["flame","tool","hot","cook"]},"droplet":{"a":"Droplet","b":"1F4A7","j":["cold","comic","drop","sweat","water","drip","faucet","spring"]},"water-wave":{"a":"Water Wave","b":"1F30A","j":["ocean","water","wave","sea","nature","tsunami","disaster"]},"jackolantern":{"a":"Jack-O-Lantern","b":"1F383","j":["celebration","halloween","jack","jack-o-lantern","lantern","jack_o_lantern","light","pumpkin","creepy","fall"]},"christmas-tree":{"a":"Christmas Tree","b":"1F384","j":["celebration","Christmas","tree","festival","vacation","december","xmas"]},"fireworks":{"a":"Fireworks","b":"1F386","j":["celebration","photo","festival","carnival","congratulations"]},"sparkler":{"a":"Sparkler","b":"1F387","j":["celebration","fireworks","sparkle","stars","night","shine"]},"firecracker":{"a":"Firecracker","b":"1F9E8","j":["dynamite","explosive","fireworks","boom","explode","explosion"]},"sparkles":{"a":"Sparkles","b":"2728","j":["*","sparkle","star","stars","shine","shiny","cool","awesome","good","magic"]},"balloon":{"a":"Balloon","b":"1F388","j":["celebration","party","birthday","circus"]},"party-popper":{"a":"Party Popper","b":"1F389","j":["celebration","party","popper","tada","congratulations","birthday","magic","circus"]},"confetti-ball":{"a":"Confetti Ball","b":"1F38A","j":["ball","celebration","confetti","festival","party","birthday","circus"]},"tanabata-tree":{"a":"Tanabata Tree","b":"1F38B","j":["banner","celebration","Japanese","tree","plant","nature","branch","summer","bamboo","wish","star_festival","tanzaku"]},"pine-decoration":{"a":"Pine Decoration","b":"1F38D","j":["bamboo","celebration","Japanese","pine","japanese","plant","nature","vegetable","panda","new_years"]},"japanese-dolls":{"a":"Japanese Dolls","b":"1F38E","j":["celebration","doll","festival","Japanese","Japanese dolls","japanese","toy","kimono"]},"carp-streamer":{"a":"Carp Streamer","b":"1F38F","j":["carp","celebration","streamer","fish","japanese","koinobori","banner"]},"wind-chime":{"a":"Wind Chime","b":"1F390","j":["bell","celebration","chime","wind","nature","ding","spring"]},"moon-viewing-ceremony":{"a":"Moon Viewing Ceremony","b":"1F391","j":["celebration","ceremony","moon","photo","japan","asia","tsukimi"]},"red-envelope":{"a":"Red Envelope","b":"1F9E7","j":["gift","good luck","hóngbāo","lai see","money"]},"ribbon":{"a":"Ribbon","b":"1F380","j":["celebration","decoration","pink","girl","bowtie"]},"wrapped-gift":{"a":"Wrapped Gift","b":"1F381","j":["box","celebration","gift","present","wrapped","birthday","christmas","xmas"]},"reminder-ribbon":{"a":"Reminder Ribbon","b":"1F397-FE0F","j":["celebration","reminder","ribbon","sports","cause","support","awareness"]},"admission-tickets":{"a":"Admission Tickets","b":"1F39F-FE0F","j":["admission","ticket","sports","concert","entrance"]},"ticket":{"a":"Ticket","b":"1F3AB","j":["admission","event","concert","pass"]},"military-medal":{"a":"Military Medal","b":"1F396-FE0F","j":["celebration","medal","military","award","winning","army"]},"trophy":{"a":"Trophy","b":"1F3C6-FE0F","j":["prize","win","award","contest","place","ftw","ceremony"]},"sports-medal":{"a":"Sports Medal","b":"1F3C5","j":["medal","award","winning"]},"1st-place-medal":{"a":"1st Place Medal","b":"1F947","j":["first","gold","medal","award","winning"]},"2nd-place-medal":{"a":"2nd Place Medal","b":"1F948","j":["medal","second","silver","award"]},"3rd-place-medal":{"a":"3rd Place Medal","b":"1F949","j":["bronze","medal","third","award"]},"soccer-ball":{"a":"Soccer Ball","b":"26BD-FE0F","j":["ball","football","soccer","sports"]},"baseball":{"a":"Baseball","b":"26BE-FE0F","j":["ball","sports","balls"]},"softball":{"a":"Softball","b":"1F94E","j":["ball","glove","underarm","sports","balls"]},"basketball":{"a":"Basketball","b":"1F3C0","j":["ball","hoop","sports","balls","NBA"]},"volleyball":{"a":"Volleyball","b":"1F3D0","j":["ball","game","sports","balls"]},"american-football":{"a":"American Football","b":"1F3C8","j":["american","ball","football","sports","balls","NFL"]},"rugby-football":{"a":"Rugby Football","b":"1F3C9","j":["ball","football","rugby","sports","team"]},"tennis":{"a":"Tennis","b":"1F3BE","j":["ball","racquet","sports","balls","green"]},"flying-disc":{"a":"Flying Disc","b":"1F94F","j":["ultimate","sports","frisbee"]},"bowling":{"a":"Bowling","b":"1F3B3","j":["ball","game","sports","fun","play"]},"cricket-game":{"a":"Cricket Game","b":"1F3CF","j":["ball","bat","game","sports"]},"field-hockey":{"a":"Field Hockey","b":"1F3D1","j":["ball","field","game","hockey","stick","sports"]},"ice-hockey":{"a":"Ice Hockey","b":"1F3D2","j":["game","hockey","ice","puck","stick","sports"]},"lacrosse":{"a":"Lacrosse","b":"1F94D","j":["ball","goal","stick","sports"]},"ping-pong":{"a":"Ping Pong","b":"1F3D3","j":["ball","bat","game","paddle","table tennis","sports","pingpong"]},"badminton":{"a":"Badminton","b":"1F3F8","j":["birdie","game","racquet","shuttlecock","sports"]},"boxing-glove":{"a":"Boxing Glove","b":"1F94A","j":["boxing","glove","sports","fighting"]},"martial-arts-uniform":{"a":"Martial Arts Uniform","b":"1F94B","j":["judo","karate","martial arts","taekwondo","uniform"]},"goal-net":{"a":"Goal Net","b":"1F945","j":["goal","net","sports"]},"flag-in-hole":{"a":"Flag in Hole","b":"26F3-FE0F","j":["golf","hole","sports","business","flag","summer"]},"ice-skate":{"a":"Ice Skate","b":"26F8-FE0F","j":["ice","skate","sports"]},"fishing-pole":{"a":"Fishing Pole","b":"1F3A3","j":["fish","pole","food","hobby","summer"]},"diving-mask":{"a":"Diving Mask","b":"1F93F","j":["diving","scuba","snorkeling","sport","ocean"]},"running-shirt":{"a":"Running Shirt","b":"1F3BD","j":["athletics","running","sash","shirt","play","pageant"]},"skis":{"a":"Skis","b":"1F3BF","j":["ski","snow","sports","winter","cold"]},"sled":{"a":"Sled","b":"1F6F7","j":["sledge","sleigh","luge","toboggan"]},"curling-stone":{"a":"Curling Stone","b":"1F94C","j":["game","rock","sports"]},"bullseye":{"a":"Bullseye","b":"1F3AF","j":["dart","direct hit","game","hit","target","direct_hit","play","bar"]},"yoyo":{"a":"Yo-Yo","b":"1FA80","j":["fluctuate","toy","yo-yo","yo_yo"]},"kite":{"a":"Kite","b":"1FA81","j":["fly","soar","wind"]},"water-pistol":{"a":"Water Pistol","b":"1F52B","j":["gun","handgun","pistol","revolver","tool","water","weapon","violence"]},"pool-8-ball":{"a":"Pool 8 Ball","b":"1F3B1","j":["8","ball","billiard","eight","game","pool","hobby","luck","magic"]},"crystal-ball":{"a":"Crystal Ball","b":"1F52E","j":["ball","crystal","fairy tale","fantasy","fortune","tool","disco","party","magic","circus","fortune_teller"]},"magic-wand":{"a":"Magic Wand","b":"1FA84","j":["magic","witch","wizard","supernature","power"]},"video-game":{"a":"Video Game","b":"1F3AE-FE0F","j":["controller","game","play","console","PS4"]},"joystick":{"a":"Joystick","b":"1F579-FE0F","j":["game","video game","play"]},"slot-machine":{"a":"Slot Machine","b":"1F3B0","j":["game","slot","bet","gamble","vegas","fruit machine","luck","casino"]},"game-die":{"a":"Game Die","b":"1F3B2","j":["dice","die","game","random","tabletop","play","luck"]},"puzzle-piece":{"a":"Puzzle Piece","b":"1F9E9","j":["clue","interlocking","jigsaw","piece","puzzle"]},"teddy-bear":{"a":"Teddy Bear","b":"1F9F8","j":["plaything","plush","stuffed","toy"]},"piata":{"a":"Piñata","b":"1FA85","j":["celebration","party","piñata","pinata","mexico","candy"]},"mirror-ball":{"a":"Mirror Ball","b":"1FAA9","j":["dance","disco","glitter","party"]},"nesting-dolls":{"a":"Nesting Dolls","b":"1FA86","j":["doll","nesting","russia","matryoshka","toy"]},"spade-suit":{"a":"Spade Suit","b":"2660-FE0F","j":["card","game","poker","cards","suits","magic"]},"heart-suit":{"a":"Heart Suit","b":"2665-FE0F","j":["card","game","poker","cards","magic","suits"]},"diamond-suit":{"a":"Diamond Suit","b":"2666-FE0F","j":["card","game","poker","cards","magic","suits"]},"club-suit":{"a":"Club Suit","b":"2663-FE0F","j":["card","game","poker","cards","magic","suits"]},"chess-pawn":{"a":"Chess Pawn","b":"265F-FE0F","j":["chess","dupe","expendable"]},"joker":{"a":"Joker","b":"1F0CF","j":["card","game","wildcard","poker","cards","play","magic"]},"mahjong-red-dragon":{"a":"Mahjong Red Dragon","b":"1F004-FE0F","j":["game","mahjong","red","play","chinese","kanji"]},"flower-playing-cards":{"a":"Flower Playing Cards","b":"1F3B4","j":["card","flower","game","Japanese","playing","sunset","red"]},"performing-arts":{"a":"Performing Arts","b":"1F3AD-FE0F","j":["art","mask","performing","theater","theatre","acting","drama"]},"framed-picture":{"a":"Framed Picture","b":"1F5BC-FE0F","j":["art","frame","museum","painting","picture","photography"]},"artist-palette":{"a":"Artist Palette","b":"1F3A8","j":["art","museum","painting","palette","design","paint","draw","colors"]},"thread":{"a":"Thread","b":"1F9F5","j":["needle","sewing","spool","string"]},"sewing-needle":{"a":"Sewing Needle","b":"1FAA1","j":["embroidery","needle","sewing","stitches","sutures","tailoring"]},"yarn":{"a":"Yarn","b":"1F9F6","j":["ball","crochet","knit"]},"knot":{"a":"Knot","b":"1FAA2","j":["rope","tangled","tie","twine","twist","scout"]},"glasses":{"a":"Glasses","b":"1F453-FE0F","j":["clothing","eye","eyeglasses","eyewear","fashion","accessories","eyesight","nerdy","dork","geek"]},"sunglasses":{"a":"Sunglasses","b":"1F576-FE0F","j":["dark","eye","eyewear","glasses","face","cool","accessories"]},"goggles":{"a":"Goggles","b":"1F97D","j":["eye protection","swimming","welding","eyes","protection","safety"]},"lab-coat":{"a":"Lab Coat","b":"1F97C","j":["doctor","experiment","scientist","chemist"]},"safety-vest":{"a":"Safety Vest","b":"1F9BA","j":["emergency","safety","vest","protection"]},"necktie":{"a":"Necktie","b":"1F454","j":["clothing","tie","shirt","suitup","formal","fashion","cloth","business"]},"tshirt":{"a":"T-Shirt","b":"1F455","j":["clothing","shirt","t-shirt","t_shirt","fashion","cloth","casual","tee"]},"jeans":{"a":"Jeans","b":"1F456","j":["clothing","pants","trousers","fashion","shopping"]},"scarf":{"a":"Scarf","b":"1F9E3","j":["neck","winter","clothes"]},"gloves":{"a":"Gloves","b":"1F9E4","j":["hand","hands","winter","clothes"]},"coat":{"a":"Coat","b":"1F9E5","j":["jacket"]},"socks":{"a":"Socks","b":"1F9E6","j":["stocking","stockings","clothes"]},"dress":{"a":"Dress","b":"1F457","j":["clothing","clothes","fashion","shopping"]},"kimono":{"a":"Kimono","b":"1F458","j":["clothing","dress","fashion","women","female","japanese"]},"sari":{"a":"Sari","b":"1F97B","j":["clothing","dress"]},"onepiece-swimsuit":{"a":"One-Piece Swimsuit","b":"1FA71","j":["bathing suit","one-piece swimsuit","one_piece_swimsuit","fashion"]},"briefs":{"a":"Briefs","b":"1FA72","j":["bathing suit","one-piece","swimsuit","underwear","clothing"]},"shorts":{"a":"Shorts","b":"1FA73","j":["bathing suit","pants","underwear","clothing"]},"bikini":{"a":"Bikini","b":"1F459","j":["clothing","swim","swimming","female","woman","girl","fashion","beach","summer"]},"womans-clothes":{"a":"Woman’S Clothes","b":"1F45A","j":["clothing","woman","woman’s clothes","woman_s_clothes","fashion","shopping_bags","female"]},"folding-hand-fan":{"a":"Folding Hand Fan","b":"1FAAD","j":["cooling","dance","fan","flutter","hot","shy","flamenco"]},"purse":{"a":"Purse","b":"1F45B","j":["clothing","coin","fashion","accessories","money","sales","shopping"]},"handbag":{"a":"Handbag","b":"1F45C","j":["bag","clothing","purse","fashion","accessory","accessories","shopping"]},"clutch-bag":{"a":"Clutch Bag","b":"1F45D","j":["bag","clothing","pouch","accessories","shopping"]},"shopping-bags":{"a":"Shopping Bags","b":"1F6CD-FE0F","j":["bag","hotel","shopping","mall","buy","purchase"]},"backpack":{"a":"Backpack","b":"1F392","j":["bag","rucksack","satchel","school","student","education"]},"thong-sandal":{"a":"Thong Sandal","b":"1FA74","j":["beach sandals","sandals","thong sandals","thongs","zōri","footwear","summer"]},"mans-shoe":{"a":"Man’S Shoe","b":"1F45E","j":["clothing","man","man’s shoe","shoe","man_s_shoe","fashion","male"]},"running-shoe":{"a":"Running Shoe","b":"1F45F","j":["athletic","clothing","shoe","sneaker","shoes","sports","sneakers"]},"hiking-boot":{"a":"Hiking Boot","b":"1F97E","j":["backpacking","boot","camping","hiking"]},"flat-shoe":{"a":"Flat Shoe","b":"1F97F","j":["ballet flat","slip-on","slipper","ballet"]},"highheeled-shoe":{"a":"High-Heeled Shoe","b":"1F460","j":["clothing","heel","high-heeled shoe","shoe","woman","high_heeled_shoe","fashion","shoes","female","pumps","stiletto"]},"womans-sandal":{"a":"Woman’S Sandal","b":"1F461","j":["clothing","sandal","shoe","woman","woman’s sandal","woman_s_sandal","shoes","fashion","flip flops"]},"ballet-shoes":{"a":"Ballet Shoes","b":"1FA70","j":["ballet","dance"]},"womans-boot":{"a":"Woman’S Boot","b":"1F462","j":["boot","clothing","shoe","woman","woman’s boot","woman_s_boot","shoes","fashion"]},"hair-pick":{"a":"Hair Pick","b":"1FAAE","j":["Afro","comb","hair","pick","afro"]},"crown":{"a":"Crown","b":"1F451","j":["clothing","king","queen","kod","leader","royalty","lord"]},"womans-hat":{"a":"Woman’S Hat","b":"1F452","j":["clothing","hat","woman","woman’s hat","woman_s_hat","fashion","accessories","female","lady","spring"]},"top-hat":{"a":"Top Hat","b":"1F3A9","j":["clothing","hat","top","tophat","magic","gentleman","classy","circus"]},"graduation-cap":{"a":"Graduation Cap","b":"1F393-FE0F","j":["cap","celebration","clothing","graduation","hat","school","college","degree","university","legal","learn","education"]},"billed-cap":{"a":"Billed Cap","b":"1F9E2","j":["baseball cap","cap","baseball"]},"military-helmet":{"a":"Military Helmet","b":"1FA96","j":["army","helmet","military","soldier","warrior","protection"]},"rescue-workers-helmet":{"a":"Rescue Worker’S Helmet","b":"26D1-FE0F","j":["aid","cross","face","hat","helmet","rescue worker’s helmet","rescue_worker_s_helmet","construction","build"]},"prayer-beads":{"a":"Prayer Beads","b":"1F4FF","j":["beads","clothing","necklace","prayer","religion","dhikr","religious"]},"lipstick":{"a":"Lipstick","b":"1F484","j":["cosmetics","makeup","female","girl","fashion","woman"]},"ring":{"a":"Ring","b":"1F48D","j":["diamond","wedding","propose","marriage","valentines","fashion","jewelry","gem","engagement"]},"gem-stone":{"a":"Gem Stone","b":"1F48E","j":["diamond","gem","jewel","blue","ruby","jewelry"]},"muted-speaker":{"a":"Muted Speaker","b":"1F507","j":["mute","quiet","silent","speaker","sound","volume","silence"]},"speaker-low-volume":{"a":"Speaker Low Volume","b":"1F508-FE0F","j":["soft","sound","volume","silence","broadcast"]},"speaker-medium-volume":{"a":"Speaker Medium Volume","b":"1F509","j":["medium","volume","speaker","broadcast"]},"speaker-high-volume":{"a":"Speaker High Volume","b":"1F50A","j":["loud","volume","noise","noisy","speaker","broadcast"]},"loudspeaker":{"a":"Loudspeaker","b":"1F4E2","j":["loud","public address","volume","sound"]},"megaphone":{"a":"Megaphone","b":"1F4E3","j":["cheering","sound","speaker","volume"]},"postal-horn":{"a":"Postal Horn","b":"1F4EF","j":["horn","post","postal","instrument","music"]},"bell":{"a":"Bell","b":"1F514","j":["sound","notification","christmas","xmas","chime"]},"bell-with-slash":{"a":"Bell with Slash","b":"1F515","j":["bell","forbidden","mute","quiet","silent","sound","volume"]},"musical-score":{"a":"Musical Score","b":"1F3BC","j":["music","score","treble","clef","compose"]},"musical-note":{"a":"Musical Note","b":"1F3B5","j":["music","note","score","tone","sound"]},"musical-notes":{"a":"Musical Notes","b":"1F3B6","j":["music","note","notes","score"]},"studio-microphone":{"a":"Studio Microphone","b":"1F399-FE0F","j":["mic","microphone","music","studio","sing","recording","artist","talkshow"]},"level-slider":{"a":"Level Slider","b":"1F39A-FE0F","j":["level","music","slider","scale"]},"control-knobs":{"a":"Control Knobs","b":"1F39B-FE0F","j":["control","knobs","music","dial"]},"microphone":{"a":"Microphone","b":"1F3A4","j":["karaoke","mic","sound","music","PA","sing","talkshow"]},"headphone":{"a":"Headphone","b":"1F3A7-FE0F","j":["earbud","music","score","gadgets"]},"radio":{"a":"Radio","b":"1F4FB-FE0F","j":["video","communication","music","podcast","program"]},"saxophone":{"a":"Saxophone","b":"1F3B7","j":["instrument","music","sax","jazz","blues"]},"accordion":{"a":"Accordion","b":"1FA97","j":["concertina","squeeze box","music"]},"guitar":{"a":"Guitar","b":"1F3B8","j":["instrument","music"]},"musical-keyboard":{"a":"Musical Keyboard","b":"1F3B9","j":["instrument","keyboard","music","piano","compose"]},"trumpet":{"a":"Trumpet","b":"1F3BA","j":["instrument","music","brass"]},"violin":{"a":"Violin","b":"1F3BB","j":["instrument","music","orchestra","symphony"]},"banjo":{"a":"Banjo","b":"1FA95","j":["music","stringed","instructment"]},"drum":{"a":"Drum","b":"1F941","j":["drumsticks","music","instrument","snare"]},"long-drum":{"a":"Long Drum","b":"1FA98","j":["beat","conga","drum","rhythm","music"]},"maracas":{"a":"Maracas","b":"1FA87","j":["instrument","music","percussion","rattle","shake"]},"flute":{"a":"Flute","b":"1FA88","j":["fife","music","pipe","recorder","woodwind","bamboo","instrument","pied piper"]},"mobile-phone":{"a":"Mobile Phone","b":"1F4F1","j":["cell","mobile","phone","telephone","technology","apple","gadgets","dial"]},"mobile-phone-with-arrow":{"a":"Mobile Phone with Arrow","b":"1F4F2","j":["arrow","cell","mobile","phone","receive","iphone","incoming"]},"telephone":{"a":"Telephone","b":"260E-FE0F","j":["phone","technology","communication","dial"]},"telephone-receiver":{"a":"Telephone Receiver","b":"1F4DE","j":["phone","receiver","telephone","technology","communication","dial"]},"pager":{"a":"Pager","b":"1F4DF-FE0F","j":["bbcall","oldschool","90s"]},"fax-machine":{"a":"Fax Machine","b":"1F4E0","j":["fax","communication","technology"]},"battery":{"a":"Battery","b":"1F50B","j":["power","energy","sustain"]},"low-battery":{"a":"Low Battery","b":"1FAAB","j":["electronic","low energy","drained","dead"]},"electric-plug":{"a":"Electric Plug","b":"1F50C","j":["electric","electricity","plug","charger","power"]},"laptop":{"a":"Laptop","b":"1F4BB-FE0F","j":["computer","pc","personal","technology","screen","display","monitor"]},"desktop-computer":{"a":"Desktop Computer","b":"1F5A5-FE0F","j":["computer","desktop","technology","computing","screen"]},"printer":{"a":"Printer","b":"1F5A8-FE0F","j":["computer","paper","ink"]},"keyboard":{"a":"Keyboard","b":"2328-FE0F","j":["computer","technology","type","input","text"]},"computer-mouse":{"a":"Computer Mouse","b":"1F5B1-FE0F","j":["computer","click"]},"trackball":{"a":"Trackball","b":"1F5B2-FE0F","j":["computer","technology","trackpad"]},"computer-disk":{"a":"Computer Disk","b":"1F4BD","j":["computer","disk","minidisk","optical","technology","record","data","90s"]},"floppy-disk":{"a":"Floppy Disk","b":"1F4BE","j":["computer","disk","floppy","oldschool","technology","save","90s","80s"]},"optical-disk":{"a":"Optical Disk","b":"1F4BF-FE0F","j":["CD","computer","disk","optical","technology","dvd","disc","90s"]},"dvd":{"a":"Dvd","b":"1F4C0","j":["Blu-ray","computer","disk","DVD","optical","cd","disc"]},"abacus":{"a":"Abacus","b":"1F9EE","j":["calculation"]},"movie-camera":{"a":"Movie Camera","b":"1F3A5","j":["camera","cinema","movie","film","record"]},"film-frames":{"a":"Film Frames","b":"1F39E-FE0F","j":["cinema","film","frames","movie"]},"film-projector":{"a":"Film Projector","b":"1F4FD-FE0F","j":["cinema","film","movie","projector","video","tape","record"]},"clapper-board":{"a":"Clapper Board","b":"1F3AC-FE0F","j":["clapper","movie","film","record"]},"television":{"a":"Television","b":"1F4FA-FE0F","j":["tv","video","technology","program","oldschool","show"]},"camera":{"a":"Camera","b":"1F4F7-FE0F","j":["video","gadgets","photography"]},"camera-with-flash":{"a":"Camera with Flash","b":"1F4F8","j":["camera","flash","video","photography","gadgets"]},"video-camera":{"a":"Video Camera","b":"1F4F9-FE0F","j":["camera","video","film","record"]},"videocassette":{"a":"Videocassette","b":"1F4FC","j":["tape","vhs","video","record","oldschool","90s","80s"]},"magnifying-glass-tilted-left":{"a":"Magnifying Glass Tilted Left","b":"1F50D-FE0F","j":["glass","magnifying","search","tool","zoom","find","detective"]},"magnifying-glass-tilted-right":{"a":"Magnifying Glass Tilted Right","b":"1F50E","j":["glass","magnifying","search","tool","zoom","find","detective"]},"candle":{"a":"Candle","b":"1F56F-FE0F","j":["light","fire","wax"]},"light-bulb":{"a":"Light Bulb","b":"1F4A1","j":["bulb","comic","electric","idea","light","electricity"]},"flashlight":{"a":"Flashlight","b":"1F526","j":["electric","light","tool","torch","dark","camping","sight","night"]},"red-paper-lantern":{"a":"Red Paper Lantern","b":"1F3EE","j":["bar","lantern","light","red","paper","halloween","spooky"]},"diya-lamp":{"a":"Diya Lamp","b":"1FA94","j":["diya","lamp","oil","lighting"]},"notebook-with-decorative-cover":{"a":"Notebook with Decorative Cover","b":"1F4D4","j":["book","cover","decorated","notebook","classroom","notes","record","paper","study"]},"closed-book":{"a":"Closed Book","b":"1F4D5","j":["book","closed","read","library","knowledge","textbook","learn"]},"open-book":{"a":"Open Book","b":"1F4D6","j":["book","open","read","library","knowledge","literature","learn","study"]},"green-book":{"a":"Green Book","b":"1F4D7","j":["book","green","read","library","knowledge","study"]},"blue-book":{"a":"Blue Book","b":"1F4D8","j":["blue","book","read","library","knowledge","learn","study"]},"orange-book":{"a":"Orange Book","b":"1F4D9","j":["book","orange","read","library","knowledge","textbook","study"]},"books":{"a":"Books","b":"1F4DA-FE0F","j":["book","literature","library","study"]},"notebook":{"a":"Notebook","b":"1F4D3","j":["stationery","record","notes","paper","study"]},"ledger":{"a":"Ledger","b":"1F4D2","j":["notebook","notes","paper"]},"page-with-curl":{"a":"Page with Curl","b":"1F4C3","j":["curl","document","page","documents","office","paper"]},"scroll":{"a":"Scroll","b":"1F4DC","j":["paper","documents","ancient","history"]},"page-facing-up":{"a":"Page Facing Up","b":"1F4C4","j":["document","page","documents","office","paper","information"]},"newspaper":{"a":"Newspaper","b":"1F4F0","j":["news","paper","press","headline"]},"rolledup-newspaper":{"a":"Rolled-Up Newspaper","b":"1F5DE-FE0F","j":["news","newspaper","paper","rolled","rolled-up newspaper","rolled_up_newspaper","press","headline"]},"bookmark-tabs":{"a":"Bookmark Tabs","b":"1F4D1","j":["bookmark","mark","marker","tabs","favorite","save","order","tidy"]},"bookmark":{"a":"Bookmark","b":"1F516","j":["mark","favorite","label","save"]},"label":{"a":"Label","b":"1F3F7-FE0F","j":["sale","tag"]},"money-bag":{"a":"Money Bag","b":"1F4B0-FE0F","j":["bag","dollar","money","moneybag","payment","coins","sale"]},"coin":{"a":"Coin","b":"1FA99","j":["gold","metal","money","silver","treasure","currency"]},"yen-banknote":{"a":"Yen Banknote","b":"1F4B4","j":["banknote","bill","currency","money","note","yen","sales","japanese","dollar"]},"dollar-banknote":{"a":"Dollar Banknote","b":"1F4B5","j":["banknote","bill","currency","dollar","money","note","sales"]},"euro-banknote":{"a":"Euro Banknote","b":"1F4B6","j":["banknote","bill","currency","euro","money","note","sales","dollar"]},"pound-banknote":{"a":"Pound Banknote","b":"1F4B7","j":["banknote","bill","currency","money","note","pound","british","sterling","sales","bills","uk","england"]},"money-with-wings":{"a":"Money with Wings","b":"1F4B8","j":["banknote","bill","fly","money","wings","dollar","bills","payment","sale"]},"credit-card":{"a":"Credit Card","b":"1F4B3-FE0F","j":["card","credit","money","sales","dollar","bill","payment","shopping"]},"receipt":{"a":"Receipt","b":"1F9FE","j":["accounting","bookkeeping","evidence","proof","expenses"]},"chart-increasing-with-yen":{"a":"Chart Increasing with Yen","b":"1F4B9","j":["chart","graph","growth","money","yen","green-square","presentation","stats"]},"envelope":{"a":"Envelope","b":"2709-FE0F","j":["email","letter","postal","inbox","communication"]},"email":{"a":"E-Mail","b":"1F4E7","j":["e-mail","letter","mail","e_mail","communication","inbox"]},"incoming-envelope":{"a":"Incoming Envelope","b":"1F4E8","j":["e-mail","email","envelope","incoming","letter","receive","inbox"]},"envelope-with-arrow":{"a":"Envelope with Arrow","b":"1F4E9","j":["arrow","e-mail","email","envelope","outgoing","communication"]},"outbox-tray":{"a":"Outbox Tray","b":"1F4E4-FE0F","j":["box","letter","mail","outbox","sent","tray","inbox","email"]},"inbox-tray":{"a":"Inbox Tray","b":"1F4E5-FE0F","j":["box","inbox","letter","mail","receive","tray","email","documents"]},"package":{"a":"Package","b":"1F4E6-FE0F","j":["box","parcel","mail","gift","cardboard","moving"]},"closed-mailbox-with-raised-flag":{"a":"Closed Mailbox with Raised Flag","b":"1F4EB-FE0F","j":["closed","mail","mailbox","postbox","email","inbox","communication"]},"closed-mailbox-with-lowered-flag":{"a":"Closed Mailbox with Lowered Flag","b":"1F4EA-FE0F","j":["closed","lowered","mail","mailbox","postbox","email","communication","inbox"]},"open-mailbox-with-raised-flag":{"a":"Open Mailbox with Raised Flag","b":"1F4EC-FE0F","j":["mail","mailbox","open","postbox","email","inbox","communication"]},"open-mailbox-with-lowered-flag":{"a":"Open Mailbox with Lowered Flag","b":"1F4ED-FE0F","j":["lowered","mail","mailbox","open","postbox","email","inbox"]},"postbox":{"a":"Postbox","b":"1F4EE","j":["mail","mailbox","email","letter","envelope"]},"ballot-box-with-ballot":{"a":"Ballot Box with Ballot","b":"1F5F3-FE0F","j":["ballot","box","election","vote"]},"pencil":{"a":"Pencil","b":"270F-FE0F","j":["stationery","write","paper","writing","school","study"]},"black-nib":{"a":"Black Nib","b":"2712-FE0F","j":["nib","pen","stationery","writing","write"]},"fountain-pen":{"a":"Fountain Pen","b":"1F58B-FE0F","j":["fountain","pen","stationery","writing","write"]},"pen":{"a":"Pen","b":"1F58A-FE0F","j":["ballpoint","stationery","writing","write"]},"paintbrush":{"a":"Paintbrush","b":"1F58C-FE0F","j":["painting","drawing","creativity","art"]},"crayon":{"a":"Crayon","b":"1F58D-FE0F","j":["drawing","creativity"]},"memo":{"a":"Memo","b":"1F4DD","j":["pencil","write","documents","stationery","paper","writing","legal","exam","quiz","test","study","compose"]},"briefcase":{"a":"Briefcase","b":"1F4BC","j":["business","documents","work","law","legal","job","career"]},"file-folder":{"a":"File Folder","b":"1F4C1","j":["file","folder","documents","business","office"]},"open-file-folder":{"a":"Open File Folder","b":"1F4C2","j":["file","folder","open","documents","load"]},"card-index-dividers":{"a":"Card Index Dividers","b":"1F5C2-FE0F","j":["card","dividers","index","organizing","business","stationery"]},"calendar":{"a":"Calendar","b":"1F4C5","j":["date","schedule"]},"tearoff-calendar":{"a":"Tear-off Calendar","b":"1F4C6","j":["calendar","tear-off calendar","tear_off_calendar","schedule","date","planning"]},"spiral-notepad":{"a":"Spiral Notepad","b":"1F5D2-FE0F","j":["note","pad","spiral","memo","stationery"]},"spiral-calendar":{"a":"Spiral Calendar","b":"1F5D3-FE0F","j":["calendar","pad","spiral","date","schedule","planning"]},"card-index":{"a":"Card Index","b":"1F4C7","j":["card","index","rolodex","business","stationery"]},"chart-increasing":{"a":"Chart Increasing","b":"1F4C8","j":["chart","graph","growth","trend","upward","presentation","stats","recovery","business","economics","money","sales","good","success"]},"chart-decreasing":{"a":"Chart Decreasing","b":"1F4C9","j":["chart","down","graph","trend","presentation","stats","recession","business","economics","money","sales","bad","failure"]},"bar-chart":{"a":"Bar Chart","b":"1F4CA","j":["bar","chart","graph","presentation","stats"]},"clipboard":{"a":"Clipboard","b":"1F4CB-FE0F","j":["stationery","documents"]},"pushpin":{"a":"Pushpin","b":"1F4CC","j":["pin","stationery","mark","here"]},"round-pushpin":{"a":"Round Pushpin","b":"1F4CD","j":["pin","pushpin","stationery","location","map","here"]},"paperclip":{"a":"Paperclip","b":"1F4CE","j":["documents","stationery"]},"linked-paperclips":{"a":"Linked Paperclips","b":"1F587-FE0F","j":["link","paperclip","documents","stationery"]},"straight-ruler":{"a":"Straight Ruler","b":"1F4CF","j":["ruler","straight edge","stationery","calculate","length","math","school","drawing","architect","sketch"]},"triangular-ruler":{"a":"Triangular Ruler","b":"1F4D0","j":["ruler","set","triangle","stationery","math","architect","sketch"]},"scissors":{"a":"Scissors","b":"2702-FE0F","j":["cutting","tool","stationery","cut"]},"card-file-box":{"a":"Card File Box","b":"1F5C3-FE0F","j":["box","card","file","business","stationery"]},"file-cabinet":{"a":"File Cabinet","b":"1F5C4-FE0F","j":["cabinet","file","filing","organizing"]},"wastebasket":{"a":"Wastebasket","b":"1F5D1-FE0F","j":["bin","trash","rubbish","garbage","toss"]},"locked":{"a":"Locked","b":"1F512-FE0F","j":["closed","security","password","padlock"]},"unlocked":{"a":"Unlocked","b":"1F513-FE0F","j":["lock","open","unlock","privacy","security"]},"locked-with-pen":{"a":"Locked with Pen","b":"1F50F","j":["ink","lock","nib","pen","privacy","security","secret"]},"locked-with-key":{"a":"Locked with Key","b":"1F510","j":["closed","key","lock","secure","security","privacy"]},"key":{"a":"Key","b":"1F511","j":["lock","password","door"]},"old-key":{"a":"Old Key","b":"1F5DD-FE0F","j":["clue","key","lock","old","door","password"]},"hammer":{"a":"Hammer","b":"1F528","j":["tool","tools","build","create"]},"axe":{"a":"Axe","b":"1FA93","j":["chop","hatchet","split","wood","tool","cut"]},"pick":{"a":"Pick","b":"26CF-FE0F","j":["mining","tool","tools","dig"]},"hammer-and-pick":{"a":"Hammer and Pick","b":"2692-FE0F","j":["hammer","pick","tool","tools","build","create"]},"hammer-and-wrench":{"a":"Hammer and Wrench","b":"1F6E0-FE0F","j":["hammer","spanner","tool","wrench","tools","build","create"]},"dagger":{"a":"Dagger","b":"1F5E1-FE0F","j":["knife","weapon"]},"crossed-swords":{"a":"Crossed Swords","b":"2694-FE0F","j":["crossed","swords","weapon"]},"bomb":{"a":"Bomb","b":"1F4A3-FE0F","j":["comic","boom","explode","explosion","terrorism"]},"boomerang":{"a":"Boomerang","b":"1FA83","j":["rebound","repercussion","weapon"]},"bow-and-arrow":{"a":"Bow and Arrow","b":"1F3F9","j":["archer","arrow","bow","Sagittarius","zodiac","sports"]},"shield":{"a":"Shield","b":"1F6E1-FE0F","j":["weapon","protection","security"]},"carpentry-saw":{"a":"Carpentry Saw","b":"1FA9A","j":["carpenter","lumber","saw","tool","cut","chop"]},"wrench":{"a":"Wrench","b":"1F527","j":["spanner","tool","tools","diy","ikea","fix","maintainer"]},"screwdriver":{"a":"Screwdriver","b":"1FA9B","j":["screw","tool","tools"]},"nut-and-bolt":{"a":"Nut and Bolt","b":"1F529","j":["bolt","nut","tool","handy","tools","fix"]},"gear":{"a":"Gear","b":"2699-FE0F","j":["cog","cogwheel","tool"]},"clamp":{"a":"Clamp","b":"1F5DC-FE0F","j":["compress","tool","vice"]},"balance-scale":{"a":"Balance Scale","b":"2696-FE0F","j":["balance","justice","Libra","scale","zodiac","law","fairness","weight"]},"white-cane":{"a":"White Cane","b":"1F9AF","j":["accessibility","blind","probing_cane"]},"link":{"a":"Link","b":"1F517","j":["rings","url"]},"broken-chain":{"a":"⊛ Broken Chain","b":"26D3-FE0F-200D-1F4A5","j":["break","breaking","broken chain","chain","cuffs","freedom"]},"chains":{"a":"Chains","b":"26D3-FE0F","j":["chain","lock","arrest"]},"hook":{"a":"Hook","b":"1FA9D","j":["catch","crook","curve","ensnare","selling point","tools"]},"toolbox":{"a":"Toolbox","b":"1F9F0","j":["chest","mechanic","tool","tools","diy","fix","maintainer"]},"magnet":{"a":"Magnet","b":"1F9F2","j":["attraction","horseshoe","magnetic"]},"ladder":{"a":"Ladder","b":"1FA9C","j":["climb","rung","step","tools"]},"alembic":{"a":"Alembic","b":"2697-FE0F","j":["chemistry","tool","distilling","science","experiment"]},"test-tube":{"a":"Test Tube","b":"1F9EA","j":["chemist","chemistry","experiment","lab","science"]},"petri-dish":{"a":"Petri Dish","b":"1F9EB","j":["bacteria","biologist","biology","culture","lab"]},"dna":{"a":"Dna","b":"1F9EC","j":["biologist","evolution","gene","genetics","life"]},"microscope":{"a":"Microscope","b":"1F52C","j":["science","tool","laboratory","experiment","zoomin","study"]},"telescope":{"a":"Telescope","b":"1F52D","j":["science","tool","stars","space","zoom","astronomy"]},"satellite-antenna":{"a":"Satellite Antenna","b":"1F4E1","j":["antenna","dish","satellite","communication","future","radio","space"]},"syringe":{"a":"Syringe","b":"1F489","j":["medicine","needle","shot","sick","health","hospital","drugs","blood","doctor","nurse"]},"drop-of-blood":{"a":"Drop of Blood","b":"1FA78","j":["bleed","blood donation","injury","medicine","menstruation","period","hurt","harm","wound"]},"pill":{"a":"Pill","b":"1F48A","j":["doctor","medicine","sick","health","pharmacy","drug"]},"adhesive-bandage":{"a":"Adhesive Bandage","b":"1FA79","j":["bandage","heal"]},"crutch":{"a":"Crutch","b":"1FA7C","j":["cane","disability","hurt","mobility aid","stick","accessibility","assist"]},"stethoscope":{"a":"Stethoscope","b":"1FA7A","j":["doctor","heart","medicine","health"]},"xray":{"a":"X-Ray","b":"1FA7B","j":["bones","doctor","medical","skeleton","x-ray","medicine"]},"door":{"a":"Door","b":"1F6AA","j":["house","entry","exit"]},"elevator":{"a":"Elevator","b":"1F6D7","j":["accessibility","hoist","lift"]},"mirror":{"a":"Mirror","b":"1FA9E","j":["reflection","reflector","speculum"]},"window":{"a":"Window","b":"1FA9F","j":["frame","fresh air","opening","transparent","view","scenery"]},"bed":{"a":"Bed","b":"1F6CF-FE0F","j":["hotel","sleep","rest"]},"couch-and-lamp":{"a":"Couch and Lamp","b":"1F6CB-FE0F","j":["couch","hotel","lamp","read","chill"]},"chair":{"a":"Chair","b":"1FA91","j":["seat","sit","furniture"]},"toilet":{"a":"Toilet","b":"1F6BD","j":["restroom","wc","washroom","bathroom","potty"]},"plunger":{"a":"Plunger","b":"1FAA0","j":["force cup","plumber","suction","toilet"]},"shower":{"a":"Shower","b":"1F6BF","j":["water","clean","bathroom"]},"bathtub":{"a":"Bathtub","b":"1F6C1","j":["bath","clean","shower","bathroom"]},"mouse-trap":{"a":"Mouse Trap","b":"1FAA4","j":["bait","mousetrap","snare","trap","cheese"]},"razor":{"a":"Razor","b":"1FA92","j":["sharp","shave","cut"]},"lotion-bottle":{"a":"Lotion Bottle","b":"1F9F4","j":["lotion","moisturizer","shampoo","sunscreen"]},"safety-pin":{"a":"Safety Pin","b":"1F9F7","j":["diaper","punk rock"]},"broom":{"a":"Broom","b":"1F9F9","j":["cleaning","sweeping","witch"]},"basket":{"a":"Basket","b":"1F9FA","j":["farming","laundry","picnic"]},"roll-of-paper":{"a":"Roll of Paper","b":"1F9FB","j":["paper towels","toilet paper","roll"]},"bucket":{"a":"Bucket","b":"1FAA3","j":["cask","pail","vat","water","container"]},"soap":{"a":"Soap","b":"1F9FC","j":["bar","bathing","cleaning","lather","soapdish"]},"bubbles":{"a":"Bubbles","b":"1FAE7","j":["burp","clean","soap","underwater","fun","carbonation","sparkling"]},"toothbrush":{"a":"Toothbrush","b":"1FAA5","j":["bathroom","brush","clean","dental","hygiene","teeth"]},"sponge":{"a":"Sponge","b":"1F9FD","j":["absorbing","cleaning","porous"]},"fire-extinguisher":{"a":"Fire Extinguisher","b":"1F9EF","j":["extinguish","fire","quench"]},"shopping-cart":{"a":"Shopping Cart","b":"1F6D2","j":["cart","shopping","trolley"]},"cigarette":{"a":"Cigarette","b":"1F6AC","j":["smoking","kills","tobacco","joint","smoke"]},"coffin":{"a":"Coffin","b":"26B0-FE0F","j":["death","vampire","dead","die","rip","graveyard","cemetery","casket","funeral","box"]},"headstone":{"a":"Headstone","b":"1FAA6","j":["cemetery","grave","graveyard","tombstone","death","rip"]},"funeral-urn":{"a":"Funeral Urn","b":"26B1-FE0F","j":["ashes","death","funeral","urn","dead","die","rip"]},"nazar-amulet":{"a":"Nazar Amulet","b":"1F9FF","j":["bead","charm","evil-eye","nazar","talisman"]},"hamsa":{"a":"Hamsa","b":"1FAAC","j":["amulet","Fatima","hand","Mary","Miriam","protection","religion"]},"moai":{"a":"Moai","b":"1F5FF","j":["face","moyai","statue","rock","easter island"]},"placard":{"a":"Placard","b":"1FAA7","j":["demonstration","picket","protest","sign","announcement"]},"identification-card":{"a":"Identification Card","b":"1FAAA","j":["credentials","ID","license","security","document"]},"atm-sign":{"a":"Atm Sign","b":"1F3E7","j":["ATM","ATM sign","automated","bank","teller","money","sales","cash","blue-square","payment"]},"litter-in-bin-sign":{"a":"Litter in Bin Sign","b":"1F6AE","j":["litter","litter bin","blue-square","sign","human","info"]},"potable-water":{"a":"Potable Water","b":"1F6B0","j":["drinking","potable","water","blue-square","liquid","restroom","cleaning","faucet"]},"wheelchair-symbol":{"a":"Wheelchair Symbol","b":"267F-FE0F","j":["access","blue-square","disabled","accessibility"]},"mens-room":{"a":"Men’S Room","b":"1F6B9-FE0F","j":["bathroom","lavatory","man","men’s room","restroom","toilet","WC","men_s_room","wc","blue-square","gender","male"]},"womens-room":{"a":"Women’S Room","b":"1F6BA-FE0F","j":["bathroom","lavatory","restroom","toilet","WC","woman","women’s room","women_s_room","purple-square","female","loo","gender"]},"restroom":{"a":"Restroom","b":"1F6BB","j":["bathroom","lavatory","toilet","WC","blue-square","refresh","wc","gender"]},"baby-symbol":{"a":"Baby Symbol","b":"1F6BC-FE0F","j":["baby","changing","orange-square","child"]},"water-closet":{"a":"Water Closet","b":"1F6BE","j":["bathroom","closet","lavatory","restroom","toilet","water","WC","blue-square"]},"passport-control":{"a":"Passport Control","b":"1F6C2","j":["control","passport","custom","blue-square"]},"customs":{"a":"Customs","b":"1F6C3","j":["passport","border","blue-square"]},"baggage-claim":{"a":"Baggage Claim","b":"1F6C4","j":["baggage","claim","blue-square","airport","transport"]},"left-luggage":{"a":"Left Luggage","b":"1F6C5","j":["baggage","locker","luggage","blue-square","travel"]},"warning":{"a":"Warning","b":"26A0-FE0F","j":["exclamation","wip","alert","error","problem","issue"]},"children-crossing":{"a":"Children Crossing","b":"1F6B8","j":["child","crossing","pedestrian","traffic","school","warning","danger","sign","driving","yellow-diamond"]},"no-entry":{"a":"No Entry","b":"26D4-FE0F","j":["entry","forbidden","no","not","prohibited","traffic","limit","security","privacy","bad","denied","stop","circle"]},"prohibited":{"a":"Prohibited","b":"1F6AB","j":["entry","forbidden","no","not","forbid","stop","limit","denied","disallow","circle"]},"no-bicycles":{"a":"No Bicycles","b":"1F6B3","j":["bicycle","bike","forbidden","no","prohibited","no_bikes","cyclist","circle"]},"no-smoking":{"a":"No Smoking","b":"1F6AD-FE0F","j":["forbidden","no","not","prohibited","smoking","cigarette","blue-square","smell","smoke"]},"no-littering":{"a":"No Littering","b":"1F6AF","j":["forbidden","litter","no","not","prohibited","trash","bin","garbage","circle"]},"nonpotable-water":{"a":"Non-Potable Water","b":"1F6B1","j":["non-drinking","non-potable","water","non_potable_water","drink","faucet","tap","circle"]},"no-pedestrians":{"a":"No Pedestrians","b":"1F6B7","j":["forbidden","no","not","pedestrian","prohibited","rules","crossing","walking","circle"]},"no-mobile-phones":{"a":"No Mobile Phones","b":"1F4F5","j":["cell","forbidden","mobile","no","phone","iphone","mute","circle"]},"no-one-under-eighteen":{"a":"No One Under Eighteen","b":"1F51E","j":["18","age restriction","eighteen","prohibited","underage","drink","pub","night","minor","circle"]},"radioactive":{"a":"Radioactive","b":"2622-FE0F","j":["sign","nuclear","danger"]},"biohazard":{"a":"Biohazard","b":"2623-FE0F","j":["sign","danger"]},"up-arrow":{"a":"Up Arrow","b":"2B06-FE0F","j":["arrow","cardinal","direction","north","blue-square","continue","top"]},"upright-arrow":{"a":"Up-Right Arrow","b":"2197-FE0F","j":["arrow","direction","intercardinal","northeast","up-right arrow","up_right_arrow","blue-square","point","diagonal"]},"right-arrow":{"a":"Right Arrow","b":"27A1-FE0F","j":["arrow","cardinal","direction","east","blue-square","next"]},"downright-arrow":{"a":"Down-Right Arrow","b":"2198-FE0F","j":["arrow","direction","down-right arrow","intercardinal","southeast","down_right_arrow","blue-square","diagonal"]},"down-arrow":{"a":"Down Arrow","b":"2B07-FE0F","j":["arrow","cardinal","direction","down","south","blue-square","bottom"]},"downleft-arrow":{"a":"Down-Left Arrow","b":"2199-FE0F","j":["arrow","direction","down-left arrow","intercardinal","southwest","down_left_arrow","blue-square","diagonal"]},"left-arrow":{"a":"Left Arrow","b":"2B05-FE0F","j":["arrow","cardinal","direction","west","blue-square","previous","back"]},"upleft-arrow":{"a":"Up-Left Arrow","b":"2196-FE0F","j":["arrow","direction","intercardinal","northwest","up-left arrow","up_left_arrow","blue-square","point","diagonal"]},"updown-arrow":{"a":"Up-Down Arrow","b":"2195-FE0F","j":["arrow","up-down arrow","up_down_arrow","blue-square","direction","way","vertical"]},"leftright-arrow":{"a":"Left-Right Arrow","b":"2194-FE0F","j":["arrow","left-right arrow","left_right_arrow","shape","direction","horizontal","sideways"]},"right-arrow-curving-left":{"a":"Right Arrow Curving Left","b":"21A9-FE0F","j":["arrow","back","return","blue-square","undo","enter"]},"left-arrow-curving-right":{"a":"Left Arrow Curving Right","b":"21AA-FE0F","j":["arrow","blue-square","return","rotate","direction"]},"right-arrow-curving-up":{"a":"Right Arrow Curving Up","b":"2934-FE0F","j":["arrow","blue-square","direction","top"]},"right-arrow-curving-down":{"a":"Right Arrow Curving Down","b":"2935-FE0F","j":["arrow","down","blue-square","direction","bottom"]},"clockwise-vertical-arrows":{"a":"Clockwise Vertical Arrows","b":"1F503","j":["arrow","clockwise","reload","sync","cycle","round","repeat"]},"counterclockwise-arrows-button":{"a":"Counterclockwise Arrows Button","b":"1F504","j":["anticlockwise","arrow","counterclockwise","withershins","blue-square","sync","cycle"]},"back-arrow":{"a":"Back Arrow","b":"1F519","j":["arrow","BACK","words","return"]},"end-arrow":{"a":"End Arrow","b":"1F51A","j":["arrow","END","words"]},"on-arrow":{"a":"On! Arrow","b":"1F51B","j":["arrow","mark","ON","ON!","words"]},"soon-arrow":{"a":"Soon Arrow","b":"1F51C","j":["arrow","SOON","words"]},"top-arrow":{"a":"Top Arrow","b":"1F51D","j":["arrow","TOP","up","words","blue-square"]},"place-of-worship":{"a":"Place of Worship","b":"1F6D0","j":["religion","worship","church","temple","prayer"]},"atom-symbol":{"a":"Atom Symbol","b":"269B-FE0F","j":["atheist","atom","science","physics","chemistry"]},"om":{"a":"Om","b":"1F549-FE0F","j":["Hindu","religion","hinduism","buddhism","sikhism","jainism"]},"star-of-david":{"a":"Star of David","b":"2721-FE0F","j":["David","Jew","Jewish","religion","star","star of David","judaism"]},"wheel-of-dharma":{"a":"Wheel of Dharma","b":"2638-FE0F","j":["Buddhist","dharma","religion","wheel","hinduism","buddhism","sikhism","jainism"]},"yin-yang":{"a":"Yin Yang","b":"262F-FE0F","j":["religion","tao","taoist","yang","yin","balance"]},"latin-cross":{"a":"Latin Cross","b":"271D-FE0F","j":["Christian","cross","religion","christianity"]},"orthodox-cross":{"a":"Orthodox Cross","b":"2626-FE0F","j":["Christian","cross","religion","suppedaneum"]},"star-and-crescent":{"a":"Star and Crescent","b":"262A-FE0F","j":["islam","Muslim","religion"]},"peace-symbol":{"a":"Peace Symbol","b":"262E-FE0F","j":["peace","hippie"]},"menorah":{"a":"Menorah","b":"1F54E","j":["candelabrum","candlestick","religion","hanukkah","candles","jewish"]},"dotted-sixpointed-star":{"a":"Dotted Six-Pointed Star","b":"1F52F","j":["dotted six-pointed star","fortune","star","dotted_six_pointed_star","purple-square","religion","jewish","hexagram"]},"khanda":{"a":"Khanda","b":"1FAAF","j":["religion","Sikh","Sikhism"]},"aries":{"a":"Aries","b":"2648-FE0F","j":["ram","zodiac","sign","purple-square","astrology"]},"taurus":{"a":"Taurus","b":"2649-FE0F","j":["bull","ox","zodiac","purple-square","sign","astrology"]},"gemini":{"a":"Gemini","b":"264A-FE0F","j":["twins","zodiac","sign","purple-square","astrology"]},"cancer":{"a":"Cancer","b":"264B-FE0F","j":["crab","zodiac","sign","purple-square","astrology"]},"leo":{"a":"Leo","b":"264C-FE0F","j":["lion","zodiac","sign","purple-square","astrology"]},"virgo":{"a":"Virgo","b":"264D-FE0F","j":["zodiac","sign","purple-square","astrology"]},"libra":{"a":"Libra","b":"264E-FE0F","j":["balance","justice","scales","zodiac","sign","purple-square","astrology"]},"scorpio":{"a":"Scorpio","b":"264F-FE0F","j":["scorpion","scorpius","zodiac","sign","purple-square","astrology"]},"sagittarius":{"a":"Sagittarius","b":"2650-FE0F","j":["archer","zodiac","sign","purple-square","astrology"]},"capricorn":{"a":"Capricorn","b":"2651-FE0F","j":["goat","zodiac","sign","purple-square","astrology"]},"aquarius":{"a":"Aquarius","b":"2652-FE0F","j":["bearer","water","zodiac","sign","purple-square","astrology"]},"pisces":{"a":"Pisces","b":"2653-FE0F","j":["fish","zodiac","purple-square","sign","astrology"]},"ophiuchus":{"a":"Ophiuchus","b":"26CE","j":["bearer","serpent","snake","zodiac","sign","purple-square","constellation","astrology"]},"shuffle-tracks-button":{"a":"Shuffle Tracks Button","b":"1F500","j":["arrow","crossed","blue-square","shuffle","music","random"]},"repeat-button":{"a":"Repeat Button","b":"1F501","j":["arrow","clockwise","repeat","loop","record"]},"repeat-single-button":{"a":"Repeat Single Button","b":"1F502","j":["arrow","clockwise","once","blue-square","loop"]},"play-button":{"a":"Play Button","b":"25B6-FE0F","j":["arrow","play","right","triangle","blue-square","direction"]},"fastforward-button":{"a":"Fast-Forward Button","b":"23E9-FE0F","j":["arrow","double","fast","fast-forward button","forward","fast_forward_button","blue-square","play","speed","continue"]},"next-track-button":{"a":"Next Track Button","b":"23ED-FE0F","j":["arrow","next scene","next track","triangle","forward","next","blue-square"]},"play-or-pause-button":{"a":"Play or Pause Button","b":"23EF-FE0F","j":["arrow","pause","play","right","triangle","blue-square"]},"reverse-button":{"a":"Reverse Button","b":"25C0-FE0F","j":["arrow","left","reverse","triangle","blue-square","direction"]},"fast-reverse-button":{"a":"Fast Reverse Button","b":"23EA-FE0F","j":["arrow","double","rewind","play","blue-square"]},"last-track-button":{"a":"Last Track Button","b":"23EE-FE0F","j":["arrow","previous scene","previous track","triangle","backward"]},"upwards-button":{"a":"Upwards Button","b":"1F53C","j":["arrow","button","blue-square","triangle","direction","point","forward","top"]},"fast-up-button":{"a":"Fast Up Button","b":"23EB","j":["arrow","double","blue-square","direction","top"]},"downwards-button":{"a":"Downwards Button","b":"1F53D","j":["arrow","button","down","blue-square","direction","bottom"]},"fast-down-button":{"a":"Fast Down Button","b":"23EC","j":["arrow","double","down","blue-square","direction","bottom"]},"pause-button":{"a":"Pause Button","b":"23F8-FE0F","j":["bar","double","pause","vertical","blue-square"]},"stop-button":{"a":"Stop Button","b":"23F9-FE0F","j":["square","stop","blue-square"]},"record-button":{"a":"Record Button","b":"23FA-FE0F","j":["circle","record","blue-square"]},"eject-button":{"a":"Eject Button","b":"23CF-FE0F","j":["eject","blue-square"]},"cinema":{"a":"Cinema","b":"1F3A6","j":["camera","film","movie","blue-square","record","curtain","stage","theater"]},"dim-button":{"a":"Dim Button","b":"1F505","j":["brightness","dim","low","sun","afternoon","warm","summer"]},"bright-button":{"a":"Bright Button","b":"1F506","j":["bright","brightness","sun","light"]},"antenna-bars":{"a":"Antenna Bars","b":"1F4F6","j":["antenna","bar","cell","mobile","phone","blue-square","reception","internet","connection","wifi","bluetooth","bars"]},"wireless":{"a":"Wireless","b":"1F6DC","j":["computer","internet","network","wi-fi","wifi","contactless","signal"]},"vibration-mode":{"a":"Vibration Mode","b":"1F4F3","j":["cell","mobile","mode","phone","telephone","vibration","orange-square"]},"mobile-phone-off":{"a":"Mobile Phone off","b":"1F4F4","j":["cell","mobile","off","phone","telephone","mute","orange-square","silence","quiet"]},"female-sign":{"a":"Female Sign","b":"2640-FE0F","j":["woman","women","lady","girl"]},"male-sign":{"a":"Male Sign","b":"2642-FE0F","j":["man","boy","men"]},"transgender-symbol":{"a":"Transgender Symbol","b":"26A7-FE0F","j":["transgender","lgbtq"]},"multiply":{"a":"Multiply","b":"2716-FE0F","j":["×","cancel","multiplication","sign","x","multiplication_sign","math","calculation"]},"plus":{"a":"Plus","b":"2795","j":["+","math","sign","plus_sign","calculation","addition","more","increase"]},"minus":{"a":"Minus","b":"2796","j":["-","−","math","sign","minus_sign","calculation","subtract","less"]},"divide":{"a":"Divide","b":"2797","j":["÷","division","math","sign","division_sign","calculation"]},"heavy-equals-sign":{"a":"Heavy Equals Sign","b":"1F7F0","j":["equality","math"]},"infinity":{"a":"Infinity","b":"267E-FE0F","j":["forever","unbounded","universal"]},"double-exclamation-mark":{"a":"Double Exclamation Mark","b":"203C-FE0F","j":["!","!!","bangbang","exclamation","mark","surprise"]},"exclamation-question-mark":{"a":"Exclamation Question Mark","b":"2049-FE0F","j":["!","!?","?","exclamation","interrobang","mark","punctuation","question","wat","surprise"]},"red-question-mark":{"a":"Red Question Mark","b":"2753-FE0F","j":["?","mark","punctuation","question","question_mark","doubt","confused"]},"white-question-mark":{"a":"White Question Mark","b":"2754","j":["?","mark","outlined","punctuation","question","doubts","gray","huh","confused"]},"white-exclamation-mark":{"a":"White Exclamation Mark","b":"2755","j":["!","exclamation","mark","outlined","punctuation","surprise","gray","wow","warning"]},"red-exclamation-mark":{"a":"Red Exclamation Mark","b":"2757-FE0F","j":["!","exclamation","mark","punctuation","exclamation_mark","heavy_exclamation_mark","danger","surprise","wow","warning"]},"wavy-dash":{"a":"Wavy Dash","b":"3030-FE0F","j":["dash","punctuation","wavy","draw","line","moustache","mustache","squiggle","scribble"]},"currency-exchange":{"a":"Currency Exchange","b":"1F4B1","j":["bank","currency","exchange","money","sales","dollar","travel"]},"heavy-dollar-sign":{"a":"Heavy Dollar Sign","b":"1F4B2","j":["currency","dollar","money","sales","payment","buck"]},"medical-symbol":{"a":"Medical Symbol","b":"2695-FE0F","j":["aesculapius","medicine","staff","health","hospital"]},"recycling-symbol":{"a":"Recycling Symbol","b":"267B-FE0F","j":["recycle","arrow","environment","garbage","trash"]},"fleurdelis":{"a":"Fleur-De-Lis","b":"269C-FE0F","j":["fleur-de-lis","fleur_de_lis","decorative","scout"]},"trident-emblem":{"a":"Trident Emblem","b":"1F531","j":["anchor","emblem","ship","tool","trident","weapon","spear"]},"name-badge":{"a":"Name Badge","b":"1F4DB","j":["badge","name","fire","forbid"]},"japanese-symbol-for-beginner":{"a":"Japanese Symbol for Beginner","b":"1F530","j":["beginner","chevron","Japanese","Japanese symbol for beginner","leaf","badge","shield"]},"hollow-red-circle":{"a":"Hollow Red Circle","b":"2B55-FE0F","j":["circle","large","o","red","round"]},"check-mark-button":{"a":"Check Mark Button","b":"2705","j":["✓","button","check","mark","green-square","ok","agree","vote","election","answer","tick"]},"check-box-with-check":{"a":"Check Box with Check","b":"2611-FE0F","j":["✓","box","check","ok","agree","confirm","black-square","vote","election","yes","tick"]},"check-mark":{"a":"Check Mark","b":"2714-FE0F","j":["✓","check","mark","ok","nike","answer","yes","tick"]},"cross-mark":{"a":"Cross Mark","b":"274C","j":["×","cancel","cross","mark","multiplication","multiply","x","no","delete","remove","red"]},"cross-mark-button":{"a":"Cross Mark Button","b":"274E","j":["×","mark","square","x","green-square","no","deny"]},"curly-loop":{"a":"Curly Loop","b":"27B0","j":["curl","loop","scribble","draw","shape","squiggle"]},"double-curly-loop":{"a":"Double Curly Loop","b":"27BF","j":["curl","double","loop","tape","cassette"]},"part-alternation-mark":{"a":"Part Alternation Mark","b":"303D-FE0F","j":["mark","part","graph","presentation","stats","business","economics","bad"]},"eightspoked-asterisk":{"a":"Eight-Spoked Asterisk","b":"2733-FE0F","j":["*","asterisk","eight-spoked asterisk","eight_spoked_asterisk","star","sparkle","green-square"]},"eightpointed-star":{"a":"Eight-Pointed Star","b":"2734-FE0F","j":["*","eight-pointed star","star","eight_pointed_star","orange-square","shape","polygon"]},"sparkle":{"a":"Sparkle","b":"2747-FE0F","j":["*","stars","green-square","awesome","good","fireworks"]},"copyright":{"a":"Copyright","b":"00A9-FE0F","j":["C","ip","license","circle","law","legal"]},"registered":{"a":"Registered","b":"00AE-FE0F","j":["R","alphabet","circle"]},"trade-mark":{"a":"Trade Mark","b":"2122-FE0F","j":["mark","TM","trademark","brand","law","legal"]},"keycap":{"a":"Keycap: *","b":"002A-FE0F-20E3","j":["keycap_","star"]},"keycap-0":{"a":"Keycap: 0","b":"0030-FE0F-20E3","j":["keycap","0","numbers","blue-square","null","zero"]},"keycap-1":{"a":"Keycap: 1","b":"0031-FE0F-20E3","j":["keycap","blue-square","numbers","1","one"]},"keycap-2":{"a":"Keycap: 2","b":"0032-FE0F-20E3","j":["keycap","numbers","2","prime","blue-square","two"]},"keycap-3":{"a":"Keycap: 3","b":"0033-FE0F-20E3","j":["keycap","3","numbers","prime","blue-square","three"]},"keycap-4":{"a":"Keycap: 4","b":"0034-FE0F-20E3","j":["keycap","4","numbers","blue-square","four"]},"keycap-5":{"a":"Keycap: 5","b":"0035-FE0F-20E3","j":["keycap","5","numbers","blue-square","prime","five"]},"keycap-6":{"a":"Keycap: 6","b":"0036-FE0F-20E3","j":["keycap","6","numbers","blue-square","six"]},"keycap-7":{"a":"Keycap: 7","b":"0037-FE0F-20E3","j":["keycap","7","numbers","blue-square","prime","seven"]},"keycap-8":{"a":"Keycap: 8","b":"0038-FE0F-20E3","j":["keycap","8","blue-square","numbers","eight"]},"keycap-9":{"a":"Keycap: 9","b":"0039-FE0F-20E3","j":["keycap","blue-square","numbers","9","nine"]},"keycap-10":{"a":"Keycap: 10","b":"1F51F","j":["keycap","numbers","10","blue-square","ten"]},"input-latin-uppercase":{"a":"Input Latin Uppercase","b":"1F520","j":["ABCD","input","latin","letters","uppercase","alphabet","words","blue-square"]},"input-latin-lowercase":{"a":"Input Latin Lowercase","b":"1F521","j":["abcd","input","latin","letters","lowercase","blue-square","alphabet"]},"input-numbers":{"a":"Input Numbers","b":"1F522","j":["1234","input","numbers","blue-square","1","2","3","4"]},"input-symbols":{"a":"Input Symbols","b":"1F523","j":["〒♪&%","input","blue-square","music","note","ampersand","percent","glyphs","characters"]},"input-latin-letters":{"a":"Input Latin Letters","b":"1F524","j":["abc","alphabet","input","latin","letters","blue-square"]},"a-button-blood-type":{"a":"A Button (Blood Type)","b":"1F170-FE0F","j":["A","A button (blood type)","blood type","a_button","red-square","alphabet","letter"]},"ab-button-blood-type":{"a":"Ab Button (Blood Type)","b":"1F18E","j":["AB","AB button (blood type)","blood type","ab_button","red-square","alphabet"]},"b-button-blood-type":{"a":"B Button (Blood Type)","b":"1F171-FE0F","j":["B","B button (blood type)","blood type","b_button","red-square","alphabet","letter"]},"cl-button":{"a":"Cl Button","b":"1F191","j":["CL","CL button","alphabet","words","red-square"]},"cool-button":{"a":"Cool Button","b":"1F192","j":["COOL","COOL button","words","blue-square"]},"free-button":{"a":"Free Button","b":"1F193","j":["FREE","FREE button","blue-square","words"]},"information":{"a":"Information","b":"2139-FE0F","j":["i","blue-square","alphabet","letter"]},"id-button":{"a":"Id Button","b":"1F194","j":["ID","ID button","identity","purple-square","words"]},"circled-m":{"a":"Circled M","b":"24C2-FE0F","j":["circle","circled M","M","alphabet","blue-circle","letter"]},"new-button":{"a":"New Button","b":"1F195","j":["NEW","NEW button","blue-square","words","start"]},"ng-button":{"a":"Ng Button","b":"1F196","j":["NG","NG button","blue-square","words","shape","icon"]},"o-button-blood-type":{"a":"O Button (Blood Type)","b":"1F17E-FE0F","j":["blood type","O","O button (blood type)","o_button","alphabet","red-square","letter"]},"ok-button":{"a":"Ok Button","b":"1F197","j":["OK","OK button","good","agree","yes","blue-square"]},"p-button":{"a":"P Button","b":"1F17F-FE0F","j":["P","P button","parking","cars","blue-square","alphabet","letter"]},"sos-button":{"a":"Sos Button","b":"1F198","j":["help","SOS","SOS button","red-square","words","emergency","911"]},"up-button":{"a":"Up! Button","b":"1F199","j":["mark","UP","UP!","UP! button","blue-square","above","high"]},"vs-button":{"a":"Vs Button","b":"1F19A","j":["versus","VS","VS button","words","orange-square"]},"japanese-here-button":{"a":"Japanese “Here” Button","b":"1F201","j":["“here”","Japanese","Japanese “here” button","katakana","ココ","blue-square","here","japanese","destination"]},"japanese-service-charge-button":{"a":"Japanese “Service Charge” Button","b":"1F202-FE0F","j":["“service charge”","Japanese","Japanese “service charge” button","katakana","サ","japanese","blue-square"]},"japanese-monthly-amount-button":{"a":"Japanese “Monthly Amount” Button","b":"1F237-FE0F","j":["“monthly amount”","ideograph","Japanese","Japanese “monthly amount” button","月","chinese","month","moon","japanese","orange-square","kanji"]},"japanese-not-free-of-charge-button":{"a":"Japanese “Not Free of Charge” Button","b":"1F236","j":["“not free of charge”","ideograph","Japanese","Japanese “not free of charge” button","有","orange-square","chinese","have","kanji"]},"japanese-reserved-button":{"a":"Japanese “Reserved” Button","b":"1F22F-FE0F","j":["“reserved”","ideograph","Japanese","Japanese “reserved” button","指","chinese","point","green-square","kanji"]},"japanese-bargain-button":{"a":"Japanese “Bargain” Button","b":"1F250","j":["“bargain”","ideograph","Japanese","Japanese “bargain” button","得","chinese","kanji","obtain","get","circle"]},"japanese-discount-button":{"a":"Japanese “Discount” Button","b":"1F239","j":["“discount”","ideograph","Japanese","Japanese “discount” button","割","cut","divide","chinese","kanji","pink-square"]},"japanese-free-of-charge-button":{"a":"Japanese “Free of Charge” Button","b":"1F21A-FE0F","j":["“free of charge”","ideograph","Japanese","Japanese “free of charge” button","無","nothing","chinese","kanji","japanese","orange-square"]},"japanese-prohibited-button":{"a":"Japanese “Prohibited” Button","b":"1F232","j":["“prohibited”","ideograph","Japanese","Japanese “prohibited” button","禁","kanji","japanese","chinese","forbidden","limit","restricted","red-square"]},"japanese-acceptable-button":{"a":"Japanese “Acceptable” Button","b":"1F251","j":["“acceptable”","ideograph","Japanese","Japanese “acceptable” button","可","ok","good","chinese","kanji","agree","yes","orange-circle"]},"japanese-application-button":{"a":"Japanese “Application” Button","b":"1F238","j":["“application”","ideograph","Japanese","Japanese “application” button","申","chinese","japanese","kanji","orange-square"]},"japanese-passing-grade-button":{"a":"Japanese “Passing Grade” Button","b":"1F234","j":["“passing grade”","ideograph","Japanese","Japanese “passing grade” button","合","japanese","chinese","join","kanji","red-square"]},"japanese-vacancy-button":{"a":"Japanese “Vacancy” Button","b":"1F233","j":["“vacancy”","ideograph","Japanese","Japanese “vacancy” button","空","kanji","japanese","chinese","empty","sky","blue-square"]},"japanese-congratulations-button":{"a":"Japanese “Congratulations” Button","b":"3297-FE0F","j":["“congratulations”","ideograph","Japanese","Japanese “congratulations” button","祝","chinese","kanji","japanese","red-circle"]},"japanese-secret-button":{"a":"Japanese “Secret” Button","b":"3299-FE0F","j":["“secret”","ideograph","Japanese","Japanese “secret” button","秘","privacy","chinese","sshh","kanji","red-circle"]},"japanese-open-for-business-button":{"a":"Japanese “Open for Business” Button","b":"1F23A","j":["“open for business”","ideograph","Japanese","Japanese “open for business” button","営","japanese","opening hours","orange-square"]},"japanese-no-vacancy-button":{"a":"Japanese “No Vacancy” Button","b":"1F235","j":["“no vacancy”","ideograph","Japanese","Japanese “no vacancy” button","満","full","chinese","japanese","red-square","kanji"]},"red-circle":{"a":"Red Circle","b":"1F534","j":["circle","geometric","red","shape","error","danger"]},"orange-circle":{"a":"Orange Circle","b":"1F7E0","j":["circle","orange","round"]},"yellow-circle":{"a":"Yellow Circle","b":"1F7E1","j":["circle","yellow","round"]},"green-circle":{"a":"Green Circle","b":"1F7E2","j":["circle","green","round"]},"blue-circle":{"a":"Blue Circle","b":"1F535","j":["blue","circle","geometric","shape","icon","button"]},"purple-circle":{"a":"Purple Circle","b":"1F7E3","j":["circle","purple","round"]},"brown-circle":{"a":"Brown Circle","b":"1F7E4","j":["brown","circle","round"]},"black-circle":{"a":"Black Circle","b":"26AB-FE0F","j":["circle","geometric","shape","button","round"]},"white-circle":{"a":"White Circle","b":"26AA-FE0F","j":["circle","geometric","shape","round"]},"red-square":{"a":"Red Square","b":"1F7E5","j":["red","square"]},"orange-square":{"a":"Orange Square","b":"1F7E7","j":["orange","square"]},"yellow-square":{"a":"Yellow Square","b":"1F7E8","j":["square","yellow"]},"green-square":{"a":"Green Square","b":"1F7E9","j":["green","square"]},"blue-square":{"a":"Blue Square","b":"1F7E6","j":["blue","square"]},"purple-square":{"a":"Purple Square","b":"1F7EA","j":["purple","square"]},"brown-square":{"a":"Brown Square","b":"1F7EB","j":["brown","square"]},"black-large-square":{"a":"Black Large Square","b":"2B1B-FE0F","j":["geometric","square","shape","icon","button"]},"white-large-square":{"a":"White Large Square","b":"2B1C-FE0F","j":["geometric","square","shape","icon","stone","button"]},"black-medium-square":{"a":"Black Medium Square","b":"25FC-FE0F","j":["geometric","square","shape","button","icon"]},"white-medium-square":{"a":"White Medium Square","b":"25FB-FE0F","j":["geometric","square","shape","stone","icon"]},"black-mediumsmall-square":{"a":"Black Medium-Small Square","b":"25FE-FE0F","j":["black medium-small square","geometric","square","black_medium_small_square","icon","shape","button"]},"white-mediumsmall-square":{"a":"White Medium-Small Square","b":"25FD-FE0F","j":["geometric","square","white medium-small square","white_medium_small_square","shape","stone","icon","button"]},"black-small-square":{"a":"Black Small Square","b":"25AA-FE0F","j":["geometric","square","shape","icon"]},"white-small-square":{"a":"White Small Square","b":"25AB-FE0F","j":["geometric","square","shape","icon"]},"large-orange-diamond":{"a":"Large Orange Diamond","b":"1F536","j":["diamond","geometric","orange","shape","jewel","gem"]},"large-blue-diamond":{"a":"Large Blue Diamond","b":"1F537","j":["blue","diamond","geometric","shape","jewel","gem"]},"small-orange-diamond":{"a":"Small Orange Diamond","b":"1F538","j":["diamond","geometric","orange","shape","jewel","gem"]},"small-blue-diamond":{"a":"Small Blue Diamond","b":"1F539","j":["blue","diamond","geometric","shape","jewel","gem"]},"red-triangle-pointed-up":{"a":"Red Triangle Pointed Up","b":"1F53A","j":["geometric","red","shape","direction","up","top"]},"red-triangle-pointed-down":{"a":"Red Triangle Pointed Down","b":"1F53B","j":["down","geometric","red","shape","direction","bottom"]},"diamond-with-a-dot":{"a":"Diamond with a Dot","b":"1F4A0","j":["comic","diamond","geometric","inside","jewel","blue","gem","crystal","fancy"]},"radio-button":{"a":"Radio Button","b":"1F518","j":["button","geometric","radio","input","old","music","circle"]},"white-square-button":{"a":"White Square Button","b":"1F533","j":["button","geometric","outlined","square","shape","input"]},"black-square-button":{"a":"Black Square Button","b":"1F532","j":["button","geometric","square","shape","input","frame"]},"chequered-flag":{"a":"Chequered Flag","b":"1F3C1","j":["checkered","chequered","racing","contest","finishline","race","gokart"]},"triangular-flag":{"a":"Triangular Flag","b":"1F6A9","j":["post","mark","milestone","place"]},"crossed-flags":{"a":"Crossed Flags","b":"1F38C","j":["celebration","cross","crossed","Japanese","japanese","nation","country","border"]},"black-flag":{"a":"Black Flag","b":"1F3F4","j":["waving","pirate"]},"white-flag":{"a":"White Flag","b":"1F3F3-FE0F","j":["waving","losing","loser","lost","surrender","give up","fail"]},"rainbow-flag":{"a":"Rainbow Flag","b":"1F3F3-FE0F-200D-1F308","j":["pride","rainbow","flag","gay","lgbt","queer","homosexual","lesbian","bisexual"]},"transgender-flag":{"a":"Transgender Flag","b":"1F3F3-FE0F-200D-26A7-FE0F","j":["flag","light blue","pink","transgender","white","pride","lgbtq"]},"pirate-flag":{"a":"Pirate Flag","b":"1F3F4-200D-2620-FE0F","j":["Jolly Roger","pirate","plunder","treasure","skull","crossbones","flag","banner"]},"flag-ascension-island":{"a":"Flag: Ascension Island","b":"1F1E6-1F1E8","j":["flag"]},"flag-andorra":{"a":"Flag: Andorra","b":"1F1E6-1F1E9","j":["flag","ad","nation","country","banner","andorra"]},"flag-united-arab-emirates":{"a":"Flag: United Arab Emirates","b":"1F1E6-1F1EA","j":["flag","united","arab","emirates","nation","country","banner","united_arab_emirates"]},"flag-afghanistan":{"a":"Flag: Afghanistan","b":"1F1E6-1F1EB","j":["flag","af","nation","country","banner","afghanistan"]},"flag-antigua--barbuda":{"a":"Flag: Antigua & Barbuda","b":"1F1E6-1F1EC","j":["flag","flag_antigua_barbuda","antigua","barbuda","nation","country","banner","antigua_barbuda"]},"flag-anguilla":{"a":"Flag: Anguilla","b":"1F1E6-1F1EE","j":["flag","ai","nation","country","banner","anguilla"]},"flag-albania":{"a":"Flag: Albania","b":"1F1E6-1F1F1","j":["flag","al","nation","country","banner","albania"]},"flag-armenia":{"a":"Flag: Armenia","b":"1F1E6-1F1F2","j":["flag","am","nation","country","banner","armenia"]},"flag-angola":{"a":"Flag: Angola","b":"1F1E6-1F1F4","j":["flag","ao","nation","country","banner","angola"]},"flag-antarctica":{"a":"Flag: Antarctica","b":"1F1E6-1F1F6","j":["flag","aq","nation","country","banner","antarctica"]},"flag-argentina":{"a":"Flag: Argentina","b":"1F1E6-1F1F7","j":["flag","ar","nation","country","banner","argentina"]},"flag-american-samoa":{"a":"Flag: American Samoa","b":"1F1E6-1F1F8","j":["flag","american","ws","nation","country","banner","american_samoa"]},"flag-austria":{"a":"Flag: Austria","b":"1F1E6-1F1F9","j":["flag","at","nation","country","banner","austria"]},"flag-australia":{"a":"Flag: Australia","b":"1F1E6-1F1FA","j":["flag","au","nation","country","banner","australia"]},"flag-aruba":{"a":"Flag: Aruba","b":"1F1E6-1F1FC","j":["flag","aw","nation","country","banner","aruba"]},"flag-land-islands":{"a":"Flag: Åland Islands","b":"1F1E6-1F1FD","j":["flag","flag_aland_islands","Åland","islands","nation","country","banner","aland_islands"]},"flag-azerbaijan":{"a":"Flag: Azerbaijan","b":"1F1E6-1F1FF","j":["flag","az","nation","country","banner","azerbaijan"]},"flag-bosnia--herzegovina":{"a":"Flag: Bosnia & Herzegovina","b":"1F1E7-1F1E6","j":["flag","flag_bosnia_herzegovina","bosnia","herzegovina","nation","country","banner","bosnia_herzegovina"]},"flag-barbados":{"a":"Flag: Barbados","b":"1F1E7-1F1E7","j":["flag","bb","nation","country","banner","barbados"]},"flag-bangladesh":{"a":"Flag: Bangladesh","b":"1F1E7-1F1E9","j":["flag","bd","nation","country","banner","bangladesh"]},"flag-belgium":{"a":"Flag: Belgium","b":"1F1E7-1F1EA","j":["flag","be","nation","country","banner","belgium"]},"flag-burkina-faso":{"a":"Flag: Burkina Faso","b":"1F1E7-1F1EB","j":["flag","burkina","faso","nation","country","banner","burkina_faso"]},"flag-bulgaria":{"a":"Flag: Bulgaria","b":"1F1E7-1F1EC","j":["flag","bg","nation","country","banner","bulgaria"]},"flag-bahrain":{"a":"Flag: Bahrain","b":"1F1E7-1F1ED","j":["flag","bh","nation","country","banner","bahrain"]},"flag-burundi":{"a":"Flag: Burundi","b":"1F1E7-1F1EE","j":["flag","bi","nation","country","banner","burundi"]},"flag-benin":{"a":"Flag: Benin","b":"1F1E7-1F1EF","j":["flag","bj","nation","country","banner","benin"]},"flag-st-barthlemy":{"a":"Flag: St. Barthélemy","b":"1F1E7-1F1F1","j":["flag","flag_st_barthelemy","saint","barthélemy","nation","country","banner","st_barthelemy"]},"flag-bermuda":{"a":"Flag: Bermuda","b":"1F1E7-1F1F2","j":["flag","bm","nation","country","banner","bermuda"]},"flag-brunei":{"a":"Flag: Brunei","b":"1F1E7-1F1F3","j":["flag","bn","darussalam","nation","country","banner","brunei"]},"flag-bolivia":{"a":"Flag: Bolivia","b":"1F1E7-1F1F4","j":["flag","bo","nation","country","banner","bolivia"]},"flag-caribbean-netherlands":{"a":"Flag: Caribbean Netherlands","b":"1F1E7-1F1F6","j":["flag","bonaire","nation","country","banner","caribbean_netherlands"]},"flag-brazil":{"a":"Flag: Brazil","b":"1F1E7-1F1F7","j":["flag","br","nation","country","banner","brazil"]},"flag-bahamas":{"a":"Flag: Bahamas","b":"1F1E7-1F1F8","j":["flag","bs","nation","country","banner","bahamas"]},"flag-bhutan":{"a":"Flag: Bhutan","b":"1F1E7-1F1F9","j":["flag","bt","nation","country","banner","bhutan"]},"flag-bouvet-island":{"a":"Flag: Bouvet Island","b":"1F1E7-1F1FB","j":["flag","norway"]},"flag-botswana":{"a":"Flag: Botswana","b":"1F1E7-1F1FC","j":["flag","bw","nation","country","banner","botswana"]},"flag-belarus":{"a":"Flag: Belarus","b":"1F1E7-1F1FE","j":["flag","by","nation","country","banner","belarus"]},"flag-belize":{"a":"Flag: Belize","b":"1F1E7-1F1FF","j":["flag","bz","nation","country","banner","belize"]},"flag-canada":{"a":"Flag: Canada","b":"1F1E8-1F1E6","j":["flag","ca","nation","country","banner","canada"]},"flag-cocos-keeling-islands":{"a":"Flag: Cocos (Keeling) Islands","b":"1F1E8-1F1E8","j":["flag","flag_cocos_islands","cocos","keeling","islands","nation","country","banner","cocos_islands"]},"flag-congo--kinshasa":{"a":"Flag: Congo - Kinshasa","b":"1F1E8-1F1E9","j":["flag","flag_congo_kinshasa","congo","democratic","republic","nation","country","banner","congo_kinshasa"]},"flag-central-african-republic":{"a":"Flag: Central African Republic","b":"1F1E8-1F1EB","j":["flag","central","african","republic","nation","country","banner","central_african_republic"]},"flag-congo--brazzaville":{"a":"Flag: Congo - Brazzaville","b":"1F1E8-1F1EC","j":["flag","flag_congo_brazzaville","congo","nation","country","banner","congo_brazzaville"]},"flag-switzerland":{"a":"Flag: Switzerland","b":"1F1E8-1F1ED","j":["flag","ch","nation","country","banner","switzerland"]},"flag-cte-divoire":{"a":"Flag: Côte D’Ivoire","b":"1F1E8-1F1EE","j":["flag","flag_cote_d_ivoire","ivory","coast","nation","country","banner","cote_d_ivoire"]},"flag-cook-islands":{"a":"Flag: Cook Islands","b":"1F1E8-1F1F0","j":["flag","cook","islands","nation","country","banner","cook_islands"]},"flag-chile":{"a":"Flag: Chile","b":"1F1E8-1F1F1","j":["flag","nation","country","banner","chile"]},"flag-cameroon":{"a":"Flag: Cameroon","b":"1F1E8-1F1F2","j":["flag","cm","nation","country","banner","cameroon"]},"flag-china":{"a":"Flag: China","b":"1F1E8-1F1F3","j":["flag","china","chinese","prc","country","nation","banner"]},"flag-colombia":{"a":"Flag: Colombia","b":"1F1E8-1F1F4","j":["flag","co","nation","country","banner","colombia"]},"flag-clipperton-island":{"a":"Flag: Clipperton Island","b":"1F1E8-1F1F5","j":["flag"]},"flag-costa-rica":{"a":"Flag: Costa Rica","b":"1F1E8-1F1F7","j":["flag","costa","rica","nation","country","banner","costa_rica"]},"flag-cuba":{"a":"Flag: Cuba","b":"1F1E8-1F1FA","j":["flag","cu","nation","country","banner","cuba"]},"flag-cape-verde":{"a":"Flag: Cape Verde","b":"1F1E8-1F1FB","j":["flag","cabo","verde","nation","country","banner","cape_verde"]},"flag-curaao":{"a":"Flag: Curaçao","b":"1F1E8-1F1FC","j":["flag","flag_curacao","curaçao","nation","country","banner","curacao"]},"flag-christmas-island":{"a":"Flag: Christmas Island","b":"1F1E8-1F1FD","j":["flag","christmas","island","nation","country","banner","christmas_island"]},"flag-cyprus":{"a":"Flag: Cyprus","b":"1F1E8-1F1FE","j":["flag","cy","nation","country","banner","cyprus"]},"flag-czechia":{"a":"Flag: Czechia","b":"1F1E8-1F1FF","j":["flag","cz","nation","country","banner","czechia"]},"flag-germany":{"a":"Flag: Germany","b":"1F1E9-1F1EA","j":["flag","german","nation","country","banner","germany"]},"flag-diego-garcia":{"a":"Flag: Diego Garcia","b":"1F1E9-1F1EC","j":["flag"]},"flag-djibouti":{"a":"Flag: Djibouti","b":"1F1E9-1F1EF","j":["flag","dj","nation","country","banner","djibouti"]},"flag-denmark":{"a":"Flag: Denmark","b":"1F1E9-1F1F0","j":["flag","dk","nation","country","banner","denmark"]},"flag-dominica":{"a":"Flag: Dominica","b":"1F1E9-1F1F2","j":["flag","dm","nation","country","banner","dominica"]},"flag-dominican-republic":{"a":"Flag: Dominican Republic","b":"1F1E9-1F1F4","j":["flag","dominican","republic","nation","country","banner","dominican_republic"]},"flag-algeria":{"a":"Flag: Algeria","b":"1F1E9-1F1FF","j":["flag","dz","nation","country","banner","algeria"]},"flag-ceuta--melilla":{"a":"Flag: Ceuta & Melilla","b":"1F1EA-1F1E6","j":["flag","flag_ceuta_melilla"]},"flag-ecuador":{"a":"Flag: Ecuador","b":"1F1EA-1F1E8","j":["flag","ec","nation","country","banner","ecuador"]},"flag-estonia":{"a":"Flag: Estonia","b":"1F1EA-1F1EA","j":["flag","ee","nation","country","banner","estonia"]},"flag-egypt":{"a":"Flag: Egypt","b":"1F1EA-1F1EC","j":["flag","eg","nation","country","banner","egypt"]},"flag-western-sahara":{"a":"Flag: Western Sahara","b":"1F1EA-1F1ED","j":["flag","western","sahara","nation","country","banner","western_sahara"]},"flag-eritrea":{"a":"Flag: Eritrea","b":"1F1EA-1F1F7","j":["flag","er","nation","country","banner","eritrea"]},"flag-spain":{"a":"Flag: Spain","b":"1F1EA-1F1F8","j":["flag","spain","nation","country","banner"]},"flag-ethiopia":{"a":"Flag: Ethiopia","b":"1F1EA-1F1F9","j":["flag","et","nation","country","banner","ethiopia"]},"flag-european-union":{"a":"Flag: European Union","b":"1F1EA-1F1FA","j":["flag","european","union","banner"]},"flag-finland":{"a":"Flag: Finland","b":"1F1EB-1F1EE","j":["flag","fi","nation","country","banner","finland"]},"flag-fiji":{"a":"Flag: Fiji","b":"1F1EB-1F1EF","j":["flag","fj","nation","country","banner","fiji"]},"flag-falkland-islands":{"a":"Flag: Falkland Islands","b":"1F1EB-1F1F0","j":["flag","falkland","islands","malvinas","nation","country","banner","falkland_islands"]},"flag-micronesia":{"a":"Flag: Micronesia","b":"1F1EB-1F1F2","j":["flag","micronesia","federated","states","nation","country","banner"]},"flag-faroe-islands":{"a":"Flag: Faroe Islands","b":"1F1EB-1F1F4","j":["flag","faroe","islands","nation","country","banner","faroe_islands"]},"flag-france":{"a":"Flag: France","b":"1F1EB-1F1F7","j":["flag","banner","nation","france","french","country"]},"flag-gabon":{"a":"Flag: Gabon","b":"1F1EC-1F1E6","j":["flag","ga","nation","country","banner","gabon"]},"flag-united-kingdom":{"a":"Flag: United Kingdom","b":"1F1EC-1F1E7","j":["flag","united","kingdom","great","britain","northern","ireland","nation","country","banner","british","UK","english","england","union jack","united_kingdom"]},"flag-grenada":{"a":"Flag: Grenada","b":"1F1EC-1F1E9","j":["flag","gd","nation","country","banner","grenada"]},"flag-georgia":{"a":"Flag: Georgia","b":"1F1EC-1F1EA","j":["flag","ge","nation","country","banner","georgia"]},"flag-french-guiana":{"a":"Flag: French Guiana","b":"1F1EC-1F1EB","j":["flag","french","guiana","nation","country","banner","french_guiana"]},"flag-guernsey":{"a":"Flag: Guernsey","b":"1F1EC-1F1EC","j":["flag","gg","nation","country","banner","guernsey"]},"flag-ghana":{"a":"Flag: Ghana","b":"1F1EC-1F1ED","j":["flag","gh","nation","country","banner","ghana"]},"flag-gibraltar":{"a":"Flag: Gibraltar","b":"1F1EC-1F1EE","j":["flag","gi","nation","country","banner","gibraltar"]},"flag-greenland":{"a":"Flag: Greenland","b":"1F1EC-1F1F1","j":["flag","gl","nation","country","banner","greenland"]},"flag-gambia":{"a":"Flag: Gambia","b":"1F1EC-1F1F2","j":["flag","gm","nation","country","banner","gambia"]},"flag-guinea":{"a":"Flag: Guinea","b":"1F1EC-1F1F3","j":["flag","gn","nation","country","banner","guinea"]},"flag-guadeloupe":{"a":"Flag: Guadeloupe","b":"1F1EC-1F1F5","j":["flag","gp","nation","country","banner","guadeloupe"]},"flag-equatorial-guinea":{"a":"Flag: Equatorial Guinea","b":"1F1EC-1F1F6","j":["flag","equatorial","gn","nation","country","banner","equatorial_guinea"]},"flag-greece":{"a":"Flag: Greece","b":"1F1EC-1F1F7","j":["flag","gr","nation","country","banner","greece"]},"flag-south-georgia--south-sandwich-islands":{"a":"Flag: South Georgia & South Sandwich Islands","b":"1F1EC-1F1F8","j":["flag","flag_south_georgia_south_sandwich_islands","south","georgia","sandwich","islands","nation","country","banner","south_georgia_south_sandwich_islands"]},"flag-guatemala":{"a":"Flag: Guatemala","b":"1F1EC-1F1F9","j":["flag","gt","nation","country","banner","guatemala"]},"flag-guam":{"a":"Flag: Guam","b":"1F1EC-1F1FA","j":["flag","gu","nation","country","banner","guam"]},"flag-guineabissau":{"a":"Flag: Guinea-Bissau","b":"1F1EC-1F1FC","j":["flag","flag_guinea_bissau","gw","bissau","nation","country","banner","guinea_bissau"]},"flag-guyana":{"a":"Flag: Guyana","b":"1F1EC-1F1FE","j":["flag","gy","nation","country","banner","guyana"]},"flag-hong-kong-sar-china":{"a":"Flag: Hong Kong Sar China","b":"1F1ED-1F1F0","j":["flag","hong","kong","nation","country","banner","hong_kong_sar_china"]},"flag-heard--mcdonald-islands":{"a":"Flag: Heard & Mcdonald Islands","b":"1F1ED-1F1F2","j":["flag","flag_heard_mcdonald_islands"]},"flag-honduras":{"a":"Flag: Honduras","b":"1F1ED-1F1F3","j":["flag","hn","nation","country","banner","honduras"]},"flag-croatia":{"a":"Flag: Croatia","b":"1F1ED-1F1F7","j":["flag","hr","nation","country","banner","croatia"]},"flag-haiti":{"a":"Flag: Haiti","b":"1F1ED-1F1F9","j":["flag","ht","nation","country","banner","haiti"]},"flag-hungary":{"a":"Flag: Hungary","b":"1F1ED-1F1FA","j":["flag","hu","nation","country","banner","hungary"]},"flag-canary-islands":{"a":"Flag: Canary Islands","b":"1F1EE-1F1E8","j":["flag","canary","islands","nation","country","banner","canary_islands"]},"flag-indonesia":{"a":"Flag: Indonesia","b":"1F1EE-1F1E9","j":["flag","nation","country","banner","indonesia"]},"flag-ireland":{"a":"Flag: Ireland","b":"1F1EE-1F1EA","j":["flag","ie","nation","country","banner","ireland"]},"flag-israel":{"a":"Flag: Israel","b":"1F1EE-1F1F1","j":["flag","il","nation","country","banner","israel"]},"flag-isle-of-man":{"a":"Flag: Isle of Man","b":"1F1EE-1F1F2","j":["flag","isle","man","nation","country","banner","isle_of_man"]},"flag-india":{"a":"Flag: India","b":"1F1EE-1F1F3","j":["flag","in","nation","country","banner","india"]},"flag-british-indian-ocean-territory":{"a":"Flag: British Indian Ocean Territory","b":"1F1EE-1F1F4","j":["flag","british","indian","ocean","territory","nation","country","banner","british_indian_ocean_territory"]},"flag-iraq":{"a":"Flag: Iraq","b":"1F1EE-1F1F6","j":["flag","iq","nation","country","banner","iraq"]},"flag-iran":{"a":"Flag: Iran","b":"1F1EE-1F1F7","j":["flag","iran","islamic","republic","nation","country","banner"]},"flag-iceland":{"a":"Flag: Iceland","b":"1F1EE-1F1F8","j":["flag","is","nation","country","banner","iceland"]},"flag-italy":{"a":"Flag: Italy","b":"1F1EE-1F1F9","j":["flag","italy","nation","country","banner"]},"flag-jersey":{"a":"Flag: Jersey","b":"1F1EF-1F1EA","j":["flag","je","nation","country","banner","jersey"]},"flag-jamaica":{"a":"Flag: Jamaica","b":"1F1EF-1F1F2","j":["flag","jm","nation","country","banner","jamaica"]},"flag-jordan":{"a":"Flag: Jordan","b":"1F1EF-1F1F4","j":["flag","jo","nation","country","banner","jordan"]},"flag-japan":{"a":"Flag: Japan","b":"1F1EF-1F1F5","j":["flag","japanese","nation","country","banner","japan","jp","ja"]},"flag-kenya":{"a":"Flag: Kenya","b":"1F1F0-1F1EA","j":["flag","ke","nation","country","banner","kenya"]},"flag-kyrgyzstan":{"a":"Flag: Kyrgyzstan","b":"1F1F0-1F1EC","j":["flag","kg","nation","country","banner","kyrgyzstan"]},"flag-cambodia":{"a":"Flag: Cambodia","b":"1F1F0-1F1ED","j":["flag","kh","nation","country","banner","cambodia"]},"flag-kiribati":{"a":"Flag: Kiribati","b":"1F1F0-1F1EE","j":["flag","ki","nation","country","banner","kiribati"]},"flag-comoros":{"a":"Flag: Comoros","b":"1F1F0-1F1F2","j":["flag","km","nation","country","banner","comoros"]},"flag-st-kitts--nevis":{"a":"Flag: St. Kitts & Nevis","b":"1F1F0-1F1F3","j":["flag","flag_st_kitts_nevis","saint","kitts","nevis","nation","country","banner","st_kitts_nevis"]},"flag-north-korea":{"a":"Flag: North Korea","b":"1F1F0-1F1F5","j":["flag","north","korea","nation","country","banner","north_korea"]},"flag-south-korea":{"a":"Flag: South Korea","b":"1F1F0-1F1F7","j":["flag","south","korea","nation","country","banner","south_korea"]},"flag-kuwait":{"a":"Flag: Kuwait","b":"1F1F0-1F1FC","j":["flag","kw","nation","country","banner","kuwait"]},"flag-cayman-islands":{"a":"Flag: Cayman Islands","b":"1F1F0-1F1FE","j":["flag","cayman","islands","nation","country","banner","cayman_islands"]},"flag-kazakhstan":{"a":"Flag: Kazakhstan","b":"1F1F0-1F1FF","j":["flag","kz","nation","country","banner","kazakhstan"]},"flag-laos":{"a":"Flag: Laos","b":"1F1F1-1F1E6","j":["flag","lao","democratic","republic","nation","country","banner","laos"]},"flag-lebanon":{"a":"Flag: Lebanon","b":"1F1F1-1F1E7","j":["flag","lb","nation","country","banner","lebanon"]},"flag-st-lucia":{"a":"Flag: St. Lucia","b":"1F1F1-1F1E8","j":["flag","saint","lucia","nation","country","banner","st_lucia"]},"flag-liechtenstein":{"a":"Flag: Liechtenstein","b":"1F1F1-1F1EE","j":["flag","li","nation","country","banner","liechtenstein"]},"flag-sri-lanka":{"a":"Flag: Sri Lanka","b":"1F1F1-1F1F0","j":["flag","sri","lanka","nation","country","banner","sri_lanka"]},"flag-liberia":{"a":"Flag: Liberia","b":"1F1F1-1F1F7","j":["flag","lr","nation","country","banner","liberia"]},"flag-lesotho":{"a":"Flag: Lesotho","b":"1F1F1-1F1F8","j":["flag","ls","nation","country","banner","lesotho"]},"flag-lithuania":{"a":"Flag: Lithuania","b":"1F1F1-1F1F9","j":["flag","lt","nation","country","banner","lithuania"]},"flag-luxembourg":{"a":"Flag: Luxembourg","b":"1F1F1-1F1FA","j":["flag","lu","nation","country","banner","luxembourg"]},"flag-latvia":{"a":"Flag: Latvia","b":"1F1F1-1F1FB","j":["flag","lv","nation","country","banner","latvia"]},"flag-libya":{"a":"Flag: Libya","b":"1F1F1-1F1FE","j":["flag","ly","nation","country","banner","libya"]},"flag-morocco":{"a":"Flag: Morocco","b":"1F1F2-1F1E6","j":["flag","ma","nation","country","banner","morocco"]},"flag-monaco":{"a":"Flag: Monaco","b":"1F1F2-1F1E8","j":["flag","mc","nation","country","banner","monaco"]},"flag-moldova":{"a":"Flag: Moldova","b":"1F1F2-1F1E9","j":["flag","moldova","republic","nation","country","banner"]},"flag-montenegro":{"a":"Flag: Montenegro","b":"1F1F2-1F1EA","j":["flag","me","nation","country","banner","montenegro"]},"flag-st-martin":{"a":"Flag: St. Martin","b":"1F1F2-1F1EB","j":["flag"]},"flag-madagascar":{"a":"Flag: Madagascar","b":"1F1F2-1F1EC","j":["flag","mg","nation","country","banner","madagascar"]},"flag-marshall-islands":{"a":"Flag: Marshall Islands","b":"1F1F2-1F1ED","j":["flag","marshall","islands","nation","country","banner","marshall_islands"]},"flag-north-macedonia":{"a":"Flag: North Macedonia","b":"1F1F2-1F1F0","j":["flag","macedonia","nation","country","banner","north_macedonia"]},"flag-mali":{"a":"Flag: Mali","b":"1F1F2-1F1F1","j":["flag","ml","nation","country","banner","mali"]},"flag-myanmar-burma":{"a":"Flag: Myanmar (Burma)","b":"1F1F2-1F1F2","j":["flag","flag_myanmar","mm","nation","country","banner","myanmar"]},"flag-mongolia":{"a":"Flag: Mongolia","b":"1F1F2-1F1F3","j":["flag","mn","nation","country","banner","mongolia"]},"flag-macao-sar-china":{"a":"Flag: Macao Sar China","b":"1F1F2-1F1F4","j":["flag","macao","nation","country","banner","macao_sar_china"]},"flag-northern-mariana-islands":{"a":"Flag: Northern Mariana Islands","b":"1F1F2-1F1F5","j":["flag","northern","mariana","islands","nation","country","banner","northern_mariana_islands"]},"flag-martinique":{"a":"Flag: Martinique","b":"1F1F2-1F1F6","j":["flag","mq","nation","country","banner","martinique"]},"flag-mauritania":{"a":"Flag: Mauritania","b":"1F1F2-1F1F7","j":["flag","mr","nation","country","banner","mauritania"]},"flag-montserrat":{"a":"Flag: Montserrat","b":"1F1F2-1F1F8","j":["flag","ms","nation","country","banner","montserrat"]},"flag-malta":{"a":"Flag: Malta","b":"1F1F2-1F1F9","j":["flag","mt","nation","country","banner","malta"]},"flag-mauritius":{"a":"Flag: Mauritius","b":"1F1F2-1F1FA","j":["flag","mu","nation","country","banner","mauritius"]},"flag-maldives":{"a":"Flag: Maldives","b":"1F1F2-1F1FB","j":["flag","mv","nation","country","banner","maldives"]},"flag-malawi":{"a":"Flag: Malawi","b":"1F1F2-1F1FC","j":["flag","mw","nation","country","banner","malawi"]},"flag-mexico":{"a":"Flag: Mexico","b":"1F1F2-1F1FD","j":["flag","mx","nation","country","banner","mexico"]},"flag-malaysia":{"a":"Flag: Malaysia","b":"1F1F2-1F1FE","j":["flag","my","nation","country","banner","malaysia"]},"flag-mozambique":{"a":"Flag: Mozambique","b":"1F1F2-1F1FF","j":["flag","mz","nation","country","banner","mozambique"]},"flag-namibia":{"a":"Flag: Namibia","b":"1F1F3-1F1E6","j":["flag","na","nation","country","banner","namibia"]},"flag-new-caledonia":{"a":"Flag: New Caledonia","b":"1F1F3-1F1E8","j":["flag","new","caledonia","nation","country","banner","new_caledonia"]},"flag-niger":{"a":"Flag: Niger","b":"1F1F3-1F1EA","j":["flag","ne","nation","country","banner","niger"]},"flag-norfolk-island":{"a":"Flag: Norfolk Island","b":"1F1F3-1F1EB","j":["flag","norfolk","island","nation","country","banner","norfolk_island"]},"flag-nigeria":{"a":"Flag: Nigeria","b":"1F1F3-1F1EC","j":["flag","nation","country","banner","nigeria"]},"flag-nicaragua":{"a":"Flag: Nicaragua","b":"1F1F3-1F1EE","j":["flag","ni","nation","country","banner","nicaragua"]},"flag-netherlands":{"a":"Flag: Netherlands","b":"1F1F3-1F1F1","j":["flag","nl","nation","country","banner","netherlands"]},"flag-norway":{"a":"Flag: Norway","b":"1F1F3-1F1F4","j":["flag","no","nation","country","banner","norway"]},"flag-nepal":{"a":"Flag: Nepal","b":"1F1F3-1F1F5","j":["flag","np","nation","country","banner","nepal"]},"flag-nauru":{"a":"Flag: Nauru","b":"1F1F3-1F1F7","j":["flag","nr","nation","country","banner","nauru"]},"flag-niue":{"a":"Flag: Niue","b":"1F1F3-1F1FA","j":["flag","nu","nation","country","banner","niue"]},"flag-new-zealand":{"a":"Flag: New Zealand","b":"1F1F3-1F1FF","j":["flag","new","zealand","nation","country","banner","new_zealand"]},"flag-oman":{"a":"Flag: Oman","b":"1F1F4-1F1F2","j":["flag","om_symbol","nation","country","banner","oman"]},"flag-panama":{"a":"Flag: Panama","b":"1F1F5-1F1E6","j":["flag","pa","nation","country","banner","panama"]},"flag-peru":{"a":"Flag: Peru","b":"1F1F5-1F1EA","j":["flag","pe","nation","country","banner","peru"]},"flag-french-polynesia":{"a":"Flag: French Polynesia","b":"1F1F5-1F1EB","j":["flag","french","polynesia","nation","country","banner","french_polynesia"]},"flag-papua-new-guinea":{"a":"Flag: Papua New Guinea","b":"1F1F5-1F1EC","j":["flag","papua","new","guinea","nation","country","banner","papua_new_guinea"]},"flag-philippines":{"a":"Flag: Philippines","b":"1F1F5-1F1ED","j":["flag","ph","nation","country","banner","philippines"]},"flag-pakistan":{"a":"Flag: Pakistan","b":"1F1F5-1F1F0","j":["flag","pk","nation","country","banner","pakistan"]},"flag-poland":{"a":"Flag: Poland","b":"1F1F5-1F1F1","j":["flag","pl","nation","country","banner","poland"]},"flag-st-pierre--miquelon":{"a":"Flag: St. Pierre & Miquelon","b":"1F1F5-1F1F2","j":["flag","flag_st_pierre_miquelon","saint","pierre","miquelon","nation","country","banner","st_pierre_miquelon"]},"flag-pitcairn-islands":{"a":"Flag: Pitcairn Islands","b":"1F1F5-1F1F3","j":["flag","pitcairn","nation","country","banner","pitcairn_islands"]},"flag-puerto-rico":{"a":"Flag: Puerto Rico","b":"1F1F5-1F1F7","j":["flag","puerto","rico","nation","country","banner","puerto_rico"]},"flag-palestinian-territories":{"a":"Flag: Palestinian Territories","b":"1F1F5-1F1F8","j":["flag","palestine","palestinian","territories","nation","country","banner","palestinian_territories"]},"flag-portugal":{"a":"Flag: Portugal","b":"1F1F5-1F1F9","j":["flag","pt","nation","country","banner","portugal"]},"flag-palau":{"a":"Flag: Palau","b":"1F1F5-1F1FC","j":["flag","pw","nation","country","banner","palau"]},"flag-paraguay":{"a":"Flag: Paraguay","b":"1F1F5-1F1FE","j":["flag","py","nation","country","banner","paraguay"]},"flag-qatar":{"a":"Flag: Qatar","b":"1F1F6-1F1E6","j":["flag","qa","nation","country","banner","qatar"]},"flag-runion":{"a":"Flag: Réunion","b":"1F1F7-1F1EA","j":["flag","flag_reunion","réunion","nation","country","banner","reunion"]},"flag-romania":{"a":"Flag: Romania","b":"1F1F7-1F1F4","j":["flag","ro","nation","country","banner","romania"]},"flag-serbia":{"a":"Flag: Serbia","b":"1F1F7-1F1F8","j":["flag","rs","nation","country","banner","serbia"]},"flag-russia":{"a":"Flag: Russia","b":"1F1F7-1F1FA","j":["flag","russian","federation","nation","country","banner","russia"]},"flag-rwanda":{"a":"Flag: Rwanda","b":"1F1F7-1F1FC","j":["flag","rw","nation","country","banner","rwanda"]},"flag-saudi-arabia":{"a":"Flag: Saudi Arabia","b":"1F1F8-1F1E6","j":["flag","nation","country","banner","saudi_arabia"]},"flag-solomon-islands":{"a":"Flag: Solomon Islands","b":"1F1F8-1F1E7","j":["flag","solomon","islands","nation","country","banner","solomon_islands"]},"flag-seychelles":{"a":"Flag: Seychelles","b":"1F1F8-1F1E8","j":["flag","sc","nation","country","banner","seychelles"]},"flag-sudan":{"a":"Flag: Sudan","b":"1F1F8-1F1E9","j":["flag","sd","nation","country","banner","sudan"]},"flag-sweden":{"a":"Flag: Sweden","b":"1F1F8-1F1EA","j":["flag","se","nation","country","banner","sweden"]},"flag-singapore":{"a":"Flag: Singapore","b":"1F1F8-1F1EC","j":["flag","sg","nation","country","banner","singapore"]},"flag-st-helena":{"a":"Flag: St. Helena","b":"1F1F8-1F1ED","j":["flag","saint","helena","ascension","tristan","cunha","nation","country","banner","st_helena"]},"flag-slovenia":{"a":"Flag: Slovenia","b":"1F1F8-1F1EE","j":["flag","si","nation","country","banner","slovenia"]},"flag-svalbard--jan-mayen":{"a":"Flag: Svalbard & Jan Mayen","b":"1F1F8-1F1EF","j":["flag","flag_svalbard_jan_mayen"]},"flag-slovakia":{"a":"Flag: Slovakia","b":"1F1F8-1F1F0","j":["flag","sk","nation","country","banner","slovakia"]},"flag-sierra-leone":{"a":"Flag: Sierra Leone","b":"1F1F8-1F1F1","j":["flag","sierra","leone","nation","country","banner","sierra_leone"]},"flag-san-marino":{"a":"Flag: San Marino","b":"1F1F8-1F1F2","j":["flag","san","marino","nation","country","banner","san_marino"]},"flag-senegal":{"a":"Flag: Senegal","b":"1F1F8-1F1F3","j":["flag","sn","nation","country","banner","senegal"]},"flag-somalia":{"a":"Flag: Somalia","b":"1F1F8-1F1F4","j":["flag","so","nation","country","banner","somalia"]},"flag-suriname":{"a":"Flag: Suriname","b":"1F1F8-1F1F7","j":["flag","sr","nation","country","banner","suriname"]},"flag-south-sudan":{"a":"Flag: South Sudan","b":"1F1F8-1F1F8","j":["flag","south","sd","nation","country","banner","south_sudan"]},"flag-so-tom--prncipe":{"a":"Flag: São Tomé & Príncipe","b":"1F1F8-1F1F9","j":["flag","flag_sao_tome_principe","sao","tome","principe","nation","country","banner","sao_tome_principe"]},"flag-el-salvador":{"a":"Flag: El Salvador","b":"1F1F8-1F1FB","j":["flag","el","salvador","nation","country","banner","el_salvador"]},"flag-sint-maarten":{"a":"Flag: Sint Maarten","b":"1F1F8-1F1FD","j":["flag","sint","maarten","dutch","nation","country","banner","sint_maarten"]},"flag-syria":{"a":"Flag: Syria","b":"1F1F8-1F1FE","j":["flag","syrian","arab","republic","nation","country","banner","syria"]},"flag-eswatini":{"a":"Flag: Eswatini","b":"1F1F8-1F1FF","j":["flag","sz","nation","country","banner","eswatini"]},"flag-tristan-da-cunha":{"a":"Flag: Tristan Da Cunha","b":"1F1F9-1F1E6","j":["flag"]},"flag-turks--caicos-islands":{"a":"Flag: Turks & Caicos Islands","b":"1F1F9-1F1E8","j":["flag","flag_turks_caicos_islands","turks","caicos","islands","nation","country","banner","turks_caicos_islands"]},"flag-chad":{"a":"Flag: Chad","b":"1F1F9-1F1E9","j":["flag","td","nation","country","banner","chad"]},"flag-french-southern-territories":{"a":"Flag: French Southern Territories","b":"1F1F9-1F1EB","j":["flag","french","southern","territories","nation","country","banner","french_southern_territories"]},"flag-togo":{"a":"Flag: Togo","b":"1F1F9-1F1EC","j":["flag","tg","nation","country","banner","togo"]},"flag-thailand":{"a":"Flag: Thailand","b":"1F1F9-1F1ED","j":["flag","th","nation","country","banner","thailand"]},"flag-tajikistan":{"a":"Flag: Tajikistan","b":"1F1F9-1F1EF","j":["flag","tj","nation","country","banner","tajikistan"]},"flag-tokelau":{"a":"Flag: Tokelau","b":"1F1F9-1F1F0","j":["flag","tk","nation","country","banner","tokelau"]},"flag-timorleste":{"a":"Flag: Timor-Leste","b":"1F1F9-1F1F1","j":["flag","flag_timor_leste","timor","leste","nation","country","banner","timor_leste"]},"flag-turkmenistan":{"a":"Flag: Turkmenistan","b":"1F1F9-1F1F2","j":["flag","nation","country","banner","turkmenistan"]},"flag-tunisia":{"a":"Flag: Tunisia","b":"1F1F9-1F1F3","j":["flag","tn","nation","country","banner","tunisia"]},"flag-tonga":{"a":"Flag: Tonga","b":"1F1F9-1F1F4","j":["flag","to","nation","country","banner","tonga"]},"flag-trkiye":{"a":"Flag: Türkiye","b":"1F1F9-1F1F7","j":["flag","flag_turkey","turkey","nation","country","banner"]},"flag-trinidad--tobago":{"a":"Flag: Trinidad & Tobago","b":"1F1F9-1F1F9","j":["flag","flag_trinidad_tobago","trinidad","tobago","nation","country","banner","trinidad_tobago"]},"flag-tuvalu":{"a":"Flag: Tuvalu","b":"1F1F9-1F1FB","j":["flag","nation","country","banner","tuvalu"]},"flag-taiwan":{"a":"Flag: Taiwan","b":"1F1F9-1F1FC","j":["flag","tw","nation","country","banner","taiwan"]},"flag-tanzania":{"a":"Flag: Tanzania","b":"1F1F9-1F1FF","j":["flag","tanzania","united","republic","nation","country","banner"]},"flag-ukraine":{"a":"Flag: Ukraine","b":"1F1FA-1F1E6","j":["flag","ua","nation","country","banner","ukraine"]},"flag-uganda":{"a":"Flag: Uganda","b":"1F1FA-1F1EC","j":["flag","ug","nation","country","banner","uganda"]},"flag-us-outlying-islands":{"a":"Flag: U.S. Outlying Islands","b":"1F1FA-1F1F2","j":["flag","flag_u_s_outlying_islands"]},"flag-united-nations":{"a":"Flag: United Nations","b":"1F1FA-1F1F3","j":["flag","un","banner"]},"flag-united-states":{"a":"Flag: United States","b":"1F1FA-1F1F8","j":["flag","united","states","america","nation","country","banner","united_states"]},"flag-uruguay":{"a":"Flag: Uruguay","b":"1F1FA-1F1FE","j":["flag","uy","nation","country","banner","uruguay"]},"flag-uzbekistan":{"a":"Flag: Uzbekistan","b":"1F1FA-1F1FF","j":["flag","uz","nation","country","banner","uzbekistan"]},"flag-vatican-city":{"a":"Flag: Vatican City","b":"1F1FB-1F1E6","j":["flag","vatican","city","nation","country","banner","vatican_city"]},"flag-st-vincent--grenadines":{"a":"Flag: St. Vincent & Grenadines","b":"1F1FB-1F1E8","j":["flag","flag_st_vincent_grenadines","saint","vincent","grenadines","nation","country","banner","st_vincent_grenadines"]},"flag-venezuela":{"a":"Flag: Venezuela","b":"1F1FB-1F1EA","j":["flag","ve","bolivarian","republic","nation","country","banner","venezuela"]},"flag-british-virgin-islands":{"a":"Flag: British Virgin Islands","b":"1F1FB-1F1EC","j":["flag","british","virgin","islands","bvi","nation","country","banner","british_virgin_islands"]},"flag-us-virgin-islands":{"a":"Flag: U.S. Virgin Islands","b":"1F1FB-1F1EE","j":["flag","flag_u_s_virgin_islands","virgin","islands","us","nation","country","banner","u_s_virgin_islands"]},"flag-vietnam":{"a":"Flag: Vietnam","b":"1F1FB-1F1F3","j":["flag","viet","nam","nation","country","banner","vietnam"]},"flag-vanuatu":{"a":"Flag: Vanuatu","b":"1F1FB-1F1FA","j":["flag","vu","nation","country","banner","vanuatu"]},"flag-wallis--futuna":{"a":"Flag: Wallis & Futuna","b":"1F1FC-1F1EB","j":["flag","flag_wallis_futuna","wallis","futuna","nation","country","banner","wallis_futuna"]},"flag-samoa":{"a":"Flag: Samoa","b":"1F1FC-1F1F8","j":["flag","ws","nation","country","banner","samoa"]},"flag-kosovo":{"a":"Flag: Kosovo","b":"1F1FD-1F1F0","j":["flag","xk","nation","country","banner","kosovo"]},"flag-yemen":{"a":"Flag: Yemen","b":"1F1FE-1F1EA","j":["flag","ye","nation","country","banner","yemen"]},"flag-mayotte":{"a":"Flag: Mayotte","b":"1F1FE-1F1F9","j":["flag","yt","nation","country","banner","mayotte"]},"flag-south-africa":{"a":"Flag: South Africa","b":"1F1FF-1F1E6","j":["flag","south","africa","nation","country","banner","south_africa"]},"flag-zambia":{"a":"Flag: Zambia","b":"1F1FF-1F1F2","j":["flag","zm","nation","country","banner","zambia"]},"flag-zimbabwe":{"a":"Flag: Zimbabwe","b":"1F1FF-1F1FC","j":["flag","zw","nation","country","banner","zimbabwe"]},"flag-england":{"a":"Flag: England","b":"1F3F4-E0067-E0062-E0065-E006E-E0067-E007F","j":["flag","english"]},"flag-scotland":{"a":"Flag: Scotland","b":"1F3F4-E0067-E0062-E0073-E0063-E0074-E007F","j":["flag","scottish"]},"flag-wales":{"a":"Flag: Wales","b":"1F3F4-E0067-E0062-E0077-E006C-E0073-E007F","j":["flag","welsh"]}},"aliases":{}} \ No newline at end of file From dfbb3122e74727080160253949b47050560f0e8f Mon Sep 17 00:00:00 2001 From: Valere Date: Mon, 2 Oct 2023 13:58:51 +0200 Subject: [PATCH 02/61] Remove legacy crypto code --- .github/workflows/build.yml | 6 +- .github/workflows/elementr.yml | 37 - .github/workflows/nightly.yml | 2 +- .github/workflows/nightly_er.yml | 46 - .github/workflows/quality.yml | 6 +- .github/workflows/tests-rust.yml | 102 - .github/workflows/tests.yml | 2 +- build.gradle | 2 +- coverage.gradle | 8 +- docs/nightly_build.md | 2 +- flavor.gradle | 20 - matrix-sdk-android-flow/build.gradle | 1 - matrix-sdk-android/build.gradle | 7 +- ...icElementAndroidToElementRMigrationTest.kt | 0 .../database/CryptoSanityMigrationTest.kt | 0 .../sdk/internal/crypto/CryptoStoreHelper.kt | 44 - .../sdk/internal/crypto/CryptoStoreTest.kt | 142 -- .../sdk/internal/crypto/PreShareKeysTest.kt | 92 - .../sdk/internal/crypto/UnwedgingTest.kt | 225 -- .../internal/crypto/verification/SASTest.kt | 609 ------ .../crypto/verification/qrcode/QrCodeTest.kt | 251 --- .../database/CryptoSanityMigrationTest.kt | 69 - .../crypto/keysbackup/BackupRecoveryKey.kt | 61 - .../session/crypto/keysbackup/BackupUtils.kt | 32 - .../MessageVerificationAcceptContent.kt | 64 - .../MessageVerificationCancelContent.kt | 50 - .../message/MessageVerificationDoneContent.kt | 46 - .../message/MessageVerificationKeyContent.kt | 52 - .../message/MessageVerificationMacContent.kt | 50 - .../MessageVerificationReadyContent.kt | 50 - .../MessageVerificationRequestContent.kt | 42 - .../MessageVerificationStartContent.kt | 45 - .../sdk/internal/crypto/CryptoModule.kt | 266 --- .../crypto/DecryptRoomEventUseCase.kt | 145 -- .../internal/crypto/DefaultCryptoService.kt | 1457 ------------- .../sdk/internal/crypto/DeviceListManager.kt | 603 ------ .../sdk/internal/crypto/EventDecryptor.kt | 283 --- .../crypto/InboundGroupSessionStore.kt | 126 -- .../crypto/IncomingKeyRequestManager.kt | 465 ---- .../sdk/internal/crypto/MXOlmDevice.kt | 1014 --------- .../sdk/internal/crypto/MyDeviceInfoHolder.kt | 80 - .../sdk/internal/crypto/ObjectSigner.kt | 54 - .../sdk/internal/crypto/OlmSessionStore.kt | 160 -- .../internal/crypto/OneTimeKeysUploader.kt | 250 --- .../crypto/OutgoingKeyRequestManager.kt | 526 ----- .../internal/crypto/RoomDecryptorProvider.kt | 106 - .../internal/crypto/RoomEncryptorsStore.kt | 60 - .../sdk/internal/crypto/SecretShareManager.kt | 344 --- .../EnsureOlmSessionsForDevicesAction.kt | 182 -- .../EnsureOlmSessionsForUsersAction.kt | 50 - .../actions/MegolmSessionDataImporter.kt | 132 -- .../crypto/actions/MessageEncrypter.kt | 89 - .../actions/SetDeviceVerificationAction.kt | 55 - .../crypto/algorithms/IMXDecrypting.kt | 47 - .../crypto/algorithms/IMXEncrypting.kt | 39 - .../crypto/algorithms/IMXGroupEncryption.kt | 54 - .../algorithms/megolm/MXMegolmDecryption.kt | 364 ---- .../megolm/MXMegolmDecryptionFactory.kt | 52 - .../algorithms/megolm/MXMegolmEncryption.kt | 613 ------ .../megolm/MXMegolmEncryptionFactory.kt | 68 - .../megolm/MXOutboundSessionInfo.kt | 77 - .../algorithms/megolm/SharedWithHelper.kt | 43 - .../megolm/UnRequestedForwardManager.kt | 150 -- .../crypto/algorithms/olm/MXOlmDecryption.kt | 269 --- .../algorithms/olm/MXOlmDecryptionFactory.kt | 34 - .../crypto/algorithms/olm/MXOlmEncryption.kt | 80 - .../algorithms/olm/MXOlmEncryptionFactory.kt | 46 - .../crypto/crosssigning/ComputeTrustTask.kt | 93 - .../crypto/crosssigning/CrossSigningOlm.kt | 91 - .../DefaultCrossSigningService.kt | 819 ------- .../crypto/crosssigning/Extensions.kt | 28 - .../crypto/crosssigning/UpdateTrustWorker.kt | 390 ---- .../keysbackup/DefaultKeysBackupService.kt | 1337 ------------ .../model/rest/KeyVerificationAccept.kt | 90 - .../model/rest/KeyVerificationCancel.kt | 57 - .../crypto/model/rest/KeyVerificationDone.kt | 32 - .../crypto/model/rest/KeyVerificationKey.kt | 48 - .../crypto/model/rest/KeyVerificationMac.kt | 42 - .../crypto/model/rest/KeyVerificationReady.kt | 37 - .../model/rest/KeyVerificationRequest.kt | 38 - .../crypto/model/rest/KeyVerificationStart.kt | 45 - .../internal/crypto/store/IMXCryptoStore.kt | 498 ----- .../crypto/store/db/RealmCryptoStore.kt | 1891 ----------------- .../store/db/RealmCryptoStoreMigration.kt | 87 - .../crypto/task/InitializeCrossSigningTask.kt | 190 -- .../DefaultVerificationService.kt | 1713 --------------- .../verification/KotlinQRVerification.kt | 82 - .../verification/KotlinSasTransaction.kt | 476 ----- .../verification/KotlinVerificationRequest.kt | 131 -- .../crypto/verification/VerificationActor.kt | 1749 --------------- .../VerificationCryptoPrimitiveProvider.kt | 35 - .../crypto/verification/VerificationInfo.kt | 33 - .../verification/VerificationInfoAccept.kt | 83 - .../verification/VerificationInfoCancel.kt | 45 - .../verification/VerificationInfoDone.kt | 26 - .../verification/VerificationInfoKey.kt | 45 - .../verification/VerificationInfoMac.kt | 53 - .../verification/VerificationInfoReady.kt | 61 - .../verification/VerificationInfoRequest.kt | 52 - .../verification/VerificationInfoStart.kt | 126 -- .../crypto/verification/VerificationIntent.kt | 162 -- .../VerificationMessageProcessor.kt | 144 -- .../verification/VerificationRequestsStore.kt | 103 - .../VerificationTransportLayer.kt | 109 - .../verification/VerificationTrustBackend.kt | 95 - .../crypto/verification/qrcode/Extensions.kt | 127 -- .../crypto/verification/qrcode/QrCodeData.kt | 105 - .../verification/qrcode/SharedSecret.kt | 29 - .../session/sync/handler/CryptoSyncHandler.kt | 130 -- .../sync/handler/ShieldSummaryUpdater.kt | 56 - .../android/sdk/session/SessionComponent.kt | 145 -- .../android/sdk/api/MatrixConfiguration.kt | 1 - .../crypto/keysbackup/BackupRecoveryKey.kt | 0 .../session/crypto/keysbackup/BackupUtils.kt | 4 +- .../MessageVerificationCancelContent.kt | 0 .../message/MessageVerificationDoneContent.kt | 0 .../message/MessageVerificationKeyContent.kt | 0 .../message/MessageVerificationMacContent.kt | 0 .../MessageVerificationReadyContent.kt | 0 .../MessageVerificationRequestContent.kt | 0 .../MessageVerificationStartContent.kt | 0 .../coroutines/builder/FlowBuilders.kt | 0 .../crypto/ComputeShieldForGroupUseCase.kt | 0 .../sdk/internal/crypto/CryptoModule.kt | 0 .../crypto/DecryptRoomEventUseCase.kt | 0 .../android/sdk/internal/crypto/Device.kt | 0 .../crypto/EncryptEventContentUseCase.kt | 0 .../internal/crypto/EnsureUsersKeysUseCase.kt | 0 .../sdk/internal/crypto/EventDecryptor.kt | 0 .../sdk/internal/crypto/FlowCollectors.kt | 0 .../internal/crypto/GetUserIdentityUseCase.kt | 0 .../android/sdk/internal/crypto/OlmMachine.kt | 0 .../crypto/PrepareToEncryptUseCase.kt | 0 .../crypto/RustCrossSigningService.kt | 0 .../sdk/internal/crypto/RustCryptoService.kt | 0 .../sdk/internal/crypto/SecretShareManager.kt | 0 .../sdk/internal/crypto/UserIdentities.kt | 0 .../megolm/UnRequestedForwardManager.kt | 0 .../crypto/crosssigning/UpdateTrustWorker.kt | 0 .../crypto/keysbackup/RustKeyBackupService.kt | 0 .../network/OutgoingRequestsProcessor.kt | 0 .../internal/crypto/network/RequestSender.kt | 0 .../internal/crypto/store/RustCryptoStore.kt | 0 .../store/db/RealmCryptoStoreMigration.kt | 0 .../store/db/RustMigrationInfoProvider.kt | 0 .../store/db/migration/MigrateCryptoTo022.kt | 0 .../rust/ExtractMigrationDataFailure.kt | 0 .../rust/ExtractMigrationDataUseCase.kt | 0 .../store/db/migration/rust/ExtractUtils.kt | 0 .../verification/RustVerificationService.kt | 0 .../crypto/verification/SasVerification.kt | 0 .../VerificationListenersHolder.kt | 0 .../verification/VerificationRequest.kt | 0 .../verification/VerificationsProvider.kt | 0 .../verification/qrcode/QrCodeVerification.kt | 0 .../session/MigrateEAtoEROperation.kt | 0 .../sdk/internal/session/SessionComponent.kt | 0 .../sync/handler/ShieldSummaryUpdater.kt | 0 .../crypto/UnRequestedKeysManagerTest.kt | 250 --- .../FakeCryptoStoreForVerification.kt | 216 -- .../verification/VerificationActorHelper.kt | 284 --- .../verification/VerificationActorTest.kt | 528 ----- vector-app/build.gradle | 25 +- .../res/values/colors.xml | 4 - .../im/vector/app/core/di/SingletonModule.kt | 1 - vector/build.gradle | 2 - .../vector/app/core/di/ConfigurationModule.kt | 18 +- .../im/vector/app/features/MainActivity.kt | 5 +- .../analytics/impl/DefaultVectorAnalytics.kt | 5 +- .../metrics/sentry/SentryCryptoAnalytics.kt | 3 +- .../KeysBackupRestoreFromKeyViewModel.kt | 2 +- .../KeysBackupRestoreSharedViewModel.kt | 2 +- .../recover/BackupToQuadSMigrationTask.kt | 3 +- .../self/SelfVerificationViewModel.kt | 3 +- 174 files changed, 28 insertions(+), 23604 deletions(-) delete mode 100644 .github/workflows/elementr.yml delete mode 100644 .github/workflows/nightly_er.yml delete mode 100644 .github/workflows/tests-rust.yml delete mode 100644 flavor.gradle rename matrix-sdk-android/src/{androidTestRustCrypto => androidTest}/java/org/matrix/android/sdk/internal/crypto/store/migration/DynamicElementAndroidToElementRMigrationTest.kt (100%) rename matrix-sdk-android/src/{androidTestRustCrypto => androidTest}/java/org/matrix/android/sdk/internal/database/CryptoSanityMigrationTest.kt (100%) delete mode 100644 matrix-sdk-android/src/androidTestKotlinCrypto/java/org/matrix/android/sdk/internal/crypto/CryptoStoreHelper.kt delete mode 100644 matrix-sdk-android/src/androidTestKotlinCrypto/java/org/matrix/android/sdk/internal/crypto/CryptoStoreTest.kt delete mode 100644 matrix-sdk-android/src/androidTestKotlinCrypto/java/org/matrix/android/sdk/internal/crypto/PreShareKeysTest.kt delete mode 100644 matrix-sdk-android/src/androidTestKotlinCrypto/java/org/matrix/android/sdk/internal/crypto/UnwedgingTest.kt delete mode 100644 matrix-sdk-android/src/androidTestKotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/SASTest.kt delete mode 100644 matrix-sdk-android/src/androidTestKotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/QrCodeTest.kt delete mode 100644 matrix-sdk-android/src/androidTestKotlinCrypto/java/org/matrix/android/sdk/internal/database/CryptoSanityMigrationTest.kt delete mode 100644 matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/api/session/crypto/keysbackup/BackupRecoveryKey.kt delete mode 100644 matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/api/session/crypto/keysbackup/BackupUtils.kt delete mode 100644 matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/api/session/room/model/message/MessageVerificationAcceptContent.kt delete mode 100644 matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/api/session/room/model/message/MessageVerificationCancelContent.kt delete mode 100644 matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/api/session/room/model/message/MessageVerificationDoneContent.kt delete mode 100644 matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/api/session/room/model/message/MessageVerificationKeyContent.kt delete mode 100644 matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/api/session/room/model/message/MessageVerificationMacContent.kt delete mode 100644 matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/api/session/room/model/message/MessageVerificationReadyContent.kt delete mode 100644 matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/api/session/room/model/message/MessageVerificationRequestContent.kt delete mode 100644 matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/api/session/room/model/message/MessageVerificationStartContent.kt delete mode 100644 matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/CryptoModule.kt delete mode 100644 matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/DecryptRoomEventUseCase.kt delete mode 100755 matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt delete mode 100755 matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/DeviceListManager.kt delete mode 100644 matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/EventDecryptor.kt delete mode 100644 matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/InboundGroupSessionStore.kt delete mode 100644 matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/IncomingKeyRequestManager.kt delete mode 100755 matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/MXOlmDevice.kt delete mode 100644 matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/MyDeviceInfoHolder.kt delete mode 100644 matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/ObjectSigner.kt delete mode 100644 matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/OlmSessionStore.kt delete mode 100644 matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/OneTimeKeysUploader.kt delete mode 100755 matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/OutgoingKeyRequestManager.kt delete mode 100644 matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/RoomDecryptorProvider.kt delete mode 100644 matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/RoomEncryptorsStore.kt delete mode 100644 matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/SecretShareManager.kt delete mode 100644 matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/actions/EnsureOlmSessionsForDevicesAction.kt delete mode 100644 matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/actions/EnsureOlmSessionsForUsersAction.kt delete mode 100644 matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/actions/MegolmSessionDataImporter.kt delete mode 100644 matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/actions/MessageEncrypter.kt delete mode 100644 matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/actions/SetDeviceVerificationAction.kt delete mode 100644 matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/algorithms/IMXDecrypting.kt delete mode 100644 matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/algorithms/IMXEncrypting.kt delete mode 100644 matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/algorithms/IMXGroupEncryption.kt delete mode 100644 matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmDecryption.kt delete mode 100644 matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmDecryptionFactory.kt delete mode 100644 matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmEncryption.kt delete mode 100644 matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmEncryptionFactory.kt delete mode 100644 matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXOutboundSessionInfo.kt delete mode 100644 matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/SharedWithHelper.kt delete mode 100644 matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/UnRequestedForwardManager.kt delete mode 100644 matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/algorithms/olm/MXOlmDecryption.kt delete mode 100644 matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/algorithms/olm/MXOlmDecryptionFactory.kt delete mode 100644 matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/algorithms/olm/MXOlmEncryption.kt delete mode 100644 matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/algorithms/olm/MXOlmEncryptionFactory.kt delete mode 100644 matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/crosssigning/ComputeTrustTask.kt delete mode 100644 matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/crosssigning/CrossSigningOlm.kt delete mode 100644 matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/crosssigning/DefaultCrossSigningService.kt delete mode 100644 matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/crosssigning/Extensions.kt delete mode 100644 matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/crosssigning/UpdateTrustWorker.kt delete mode 100644 matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/keysbackup/DefaultKeysBackupService.kt delete mode 100644 matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/model/rest/KeyVerificationAccept.kt delete mode 100644 matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/model/rest/KeyVerificationCancel.kt delete mode 100644 matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/model/rest/KeyVerificationDone.kt delete mode 100644 matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/model/rest/KeyVerificationKey.kt delete mode 100644 matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/model/rest/KeyVerificationMac.kt delete mode 100644 matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/model/rest/KeyVerificationReady.kt delete mode 100644 matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/model/rest/KeyVerificationRequest.kt delete mode 100644 matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/model/rest/KeyVerificationStart.kt delete mode 100644 matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/store/IMXCryptoStore.kt delete mode 100644 matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStore.kt delete mode 100644 matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStoreMigration.kt delete mode 100644 matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/task/InitializeCrossSigningTask.kt delete mode 100644 matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/DefaultVerificationService.kt delete mode 100644 matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/KotlinQRVerification.kt delete mode 100644 matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/KotlinSasTransaction.kt delete mode 100644 matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/KotlinVerificationRequest.kt delete mode 100644 matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/VerificationActor.kt delete mode 100644 matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/VerificationCryptoPrimitiveProvider.kt delete mode 100644 matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/VerificationInfo.kt delete mode 100644 matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/VerificationInfoAccept.kt delete mode 100644 matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/VerificationInfoCancel.kt delete mode 100644 matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/VerificationInfoDone.kt delete mode 100644 matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/VerificationInfoKey.kt delete mode 100644 matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/VerificationInfoMac.kt delete mode 100644 matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/VerificationInfoReady.kt delete mode 100644 matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/VerificationInfoRequest.kt delete mode 100644 matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/VerificationInfoStart.kt delete mode 100644 matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/VerificationIntent.kt delete mode 100644 matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/VerificationMessageProcessor.kt delete mode 100644 matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/VerificationRequestsStore.kt delete mode 100644 matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/VerificationTransportLayer.kt delete mode 100644 matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/VerificationTrustBackend.kt delete mode 100644 matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/Extensions.kt delete mode 100644 matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/QrCodeData.kt delete mode 100644 matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/SharedSecret.kt delete mode 100644 matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/session/sync/handler/CryptoSyncHandler.kt delete mode 100644 matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/session/sync/handler/ShieldSummaryUpdater.kt delete mode 100644 matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/session/SessionComponent.kt rename matrix-sdk-android/src/{rustCrypto/java/org/matrix/android/sdk/internal => main/java/org/matrix/android/sdk/api/session}/crypto/keysbackup/BackupRecoveryKey.kt (100%) rename matrix-sdk-android/src/{rustCrypto => main}/java/org/matrix/android/sdk/api/session/crypto/keysbackup/BackupUtils.kt (82%) rename matrix-sdk-android/src/{rustCrypto => main}/java/org/matrix/android/sdk/api/session/room/model/message/MessageVerificationCancelContent.kt (100%) rename matrix-sdk-android/src/{rustCrypto => main}/java/org/matrix/android/sdk/api/session/room/model/message/MessageVerificationDoneContent.kt (100%) rename matrix-sdk-android/src/{rustCrypto => main}/java/org/matrix/android/sdk/api/session/room/model/message/MessageVerificationKeyContent.kt (100%) rename matrix-sdk-android/src/{rustCrypto => main}/java/org/matrix/android/sdk/api/session/room/model/message/MessageVerificationMacContent.kt (100%) rename matrix-sdk-android/src/{rustCrypto => main}/java/org/matrix/android/sdk/api/session/room/model/message/MessageVerificationReadyContent.kt (100%) rename matrix-sdk-android/src/{rustCrypto => main}/java/org/matrix/android/sdk/api/session/room/model/message/MessageVerificationRequestContent.kt (100%) rename matrix-sdk-android/src/{rustCrypto => main}/java/org/matrix/android/sdk/api/session/room/model/message/MessageVerificationStartContent.kt (100%) rename matrix-sdk-android/src/{rustCrypto => main}/java/org/matrix/android/sdk/internal/coroutines/builder/FlowBuilders.kt (100%) rename matrix-sdk-android/src/{rustCrypto => main}/java/org/matrix/android/sdk/internal/crypto/ComputeShieldForGroupUseCase.kt (100%) rename matrix-sdk-android/src/{rustCrypto => main}/java/org/matrix/android/sdk/internal/crypto/CryptoModule.kt (100%) rename matrix-sdk-android/src/{rustCrypto => main}/java/org/matrix/android/sdk/internal/crypto/DecryptRoomEventUseCase.kt (100%) rename matrix-sdk-android/src/{rustCrypto => main}/java/org/matrix/android/sdk/internal/crypto/Device.kt (100%) rename matrix-sdk-android/src/{rustCrypto => main}/java/org/matrix/android/sdk/internal/crypto/EncryptEventContentUseCase.kt (100%) rename matrix-sdk-android/src/{rustCrypto => main}/java/org/matrix/android/sdk/internal/crypto/EnsureUsersKeysUseCase.kt (100%) rename matrix-sdk-android/src/{rustCrypto => main}/java/org/matrix/android/sdk/internal/crypto/EventDecryptor.kt (100%) rename matrix-sdk-android/src/{rustCrypto => main}/java/org/matrix/android/sdk/internal/crypto/FlowCollectors.kt (100%) rename matrix-sdk-android/src/{rustCrypto => main}/java/org/matrix/android/sdk/internal/crypto/GetUserIdentityUseCase.kt (100%) rename matrix-sdk-android/src/{rustCrypto => main}/java/org/matrix/android/sdk/internal/crypto/OlmMachine.kt (100%) rename matrix-sdk-android/src/{rustCrypto => main}/java/org/matrix/android/sdk/internal/crypto/PrepareToEncryptUseCase.kt (100%) rename matrix-sdk-android/src/{rustCrypto => main}/java/org/matrix/android/sdk/internal/crypto/RustCrossSigningService.kt (100%) rename matrix-sdk-android/src/{rustCrypto => main}/java/org/matrix/android/sdk/internal/crypto/RustCryptoService.kt (100%) rename matrix-sdk-android/src/{rustCrypto => main}/java/org/matrix/android/sdk/internal/crypto/SecretShareManager.kt (100%) rename matrix-sdk-android/src/{rustCrypto => main}/java/org/matrix/android/sdk/internal/crypto/UserIdentities.kt (100%) rename matrix-sdk-android/src/{rustCrypto => main}/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/UnRequestedForwardManager.kt (100%) rename matrix-sdk-android/src/{rustCrypto => main}/java/org/matrix/android/sdk/internal/crypto/crosssigning/UpdateTrustWorker.kt (100%) rename matrix-sdk-android/src/{rustCrypto => main}/java/org/matrix/android/sdk/internal/crypto/keysbackup/RustKeyBackupService.kt (100%) rename matrix-sdk-android/src/{rustCrypto => main}/java/org/matrix/android/sdk/internal/crypto/network/OutgoingRequestsProcessor.kt (100%) rename matrix-sdk-android/src/{rustCrypto => main}/java/org/matrix/android/sdk/internal/crypto/network/RequestSender.kt (100%) rename matrix-sdk-android/src/{rustCrypto => main}/java/org/matrix/android/sdk/internal/crypto/store/RustCryptoStore.kt (100%) rename matrix-sdk-android/src/{rustCrypto => main}/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStoreMigration.kt (100%) rename matrix-sdk-android/src/{rustCrypto => main}/java/org/matrix/android/sdk/internal/crypto/store/db/RustMigrationInfoProvider.kt (100%) rename matrix-sdk-android/src/{rustCrypto => main}/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo022.kt (100%) rename matrix-sdk-android/src/{rustCrypto => main}/java/org/matrix/android/sdk/internal/crypto/store/db/migration/rust/ExtractMigrationDataFailure.kt (100%) rename matrix-sdk-android/src/{rustCrypto => main}/java/org/matrix/android/sdk/internal/crypto/store/db/migration/rust/ExtractMigrationDataUseCase.kt (100%) rename matrix-sdk-android/src/{rustCrypto => main}/java/org/matrix/android/sdk/internal/crypto/store/db/migration/rust/ExtractUtils.kt (100%) rename matrix-sdk-android/src/{rustCrypto => main}/java/org/matrix/android/sdk/internal/crypto/verification/RustVerificationService.kt (100%) rename matrix-sdk-android/src/{rustCrypto => main}/java/org/matrix/android/sdk/internal/crypto/verification/SasVerification.kt (100%) rename matrix-sdk-android/src/{rustCrypto => main}/java/org/matrix/android/sdk/internal/crypto/verification/VerificationListenersHolder.kt (100%) rename matrix-sdk-android/src/{rustCrypto => main}/java/org/matrix/android/sdk/internal/crypto/verification/VerificationRequest.kt (100%) rename matrix-sdk-android/src/{rustCrypto => main}/java/org/matrix/android/sdk/internal/crypto/verification/VerificationsProvider.kt (100%) rename matrix-sdk-android/src/{rustCrypto => main}/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/QrCodeVerification.kt (100%) rename matrix-sdk-android/src/{rustCrypto => main}/java/org/matrix/android/sdk/internal/session/MigrateEAtoEROperation.kt (100%) rename matrix-sdk-android/src/{rustCrypto => main}/java/org/matrix/android/sdk/internal/session/SessionComponent.kt (100%) rename matrix-sdk-android/src/{rustCrypto => main}/java/org/matrix/android/sdk/internal/session/sync/handler/ShieldSummaryUpdater.kt (100%) delete mode 100644 matrix-sdk-android/src/testKotlinCrypto/java/org/matrix/android/sdk/internal/crypto/UnRequestedKeysManagerTest.kt delete mode 100644 matrix-sdk-android/src/testKotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/FakeCryptoStoreForVerification.kt delete mode 100644 matrix-sdk-android/src/testKotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/VerificationActorHelper.kt delete mode 100644 matrix-sdk-android/src/testKotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/VerificationActorTest.kt delete mode 100644 vector-app/src/fdroidRustCryptoNightly/res/values/colors.xml diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 80e09ffdeb..942965041a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -33,7 +33,7 @@ jobs: with: cache-read-only: ${{ github.ref != 'refs/heads/develop' }} - name: Assemble ${{ matrix.target }} debug apk - run: ./gradlew assemble${{ matrix.target }}RustCryptoDebug $CI_GRADLE_ARG_PROPERTIES + run: ./gradlew assemble${{ matrix.target }}Debug $CI_GRADLE_ARG_PROPERTIES - name: Upload ${{ matrix.target }} debug APKs uses: actions/upload-artifact@v3 with: @@ -57,7 +57,7 @@ jobs: with: cache-read-only: ${{ github.ref != 'refs/heads/develop' }} - name: Assemble GPlay unsigned apk - run: ./gradlew clean assembleGplayRustCryptoRelease $CI_GRADLE_ARG_PROPERTIES + run: ./gradlew clean assembleGplayRelease $CI_GRADLE_ARG_PROPERTIES - name: Upload Gplay unsigned APKs uses: actions/upload-artifact@v3 with: @@ -79,7 +79,7 @@ jobs: - name: Execute exodus-standalone uses: docker://exodusprivacy/exodus-standalone:latest with: - args: /github/workspace/gplayRustCrypto/release/vector-gplay-rustCrypto-universal-release-unsigned.apk -j -o /github/workspace/exodus.json + args: /github/workspace/gplay/release/vector-gplay-universal-release-unsigned.apk -j -o /github/workspace/exodus.json - name: Upload exodus json report uses: actions/upload-artifact@v3 with: diff --git a/.github/workflows/elementr.yml b/.github/workflows/elementr.yml deleted file mode 100644 index c5fc3a16ca..0000000000 --- a/.github/workflows/elementr.yml +++ /dev/null @@ -1,37 +0,0 @@ -name: ER APK Build - -on: - pull_request: { } - push: - branches: [ develop ] - -# Enrich gradle.properties for CI/CD -env: - GRADLE_OPTS: -Dorg.gradle.jvmargs="-Xmx3072m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError" -Dkotlin.daemon.jvm.options="-Xmx2560m" -Dkotlin.incremental=false - CI_GRADLE_ARG_PROPERTIES: --stacktrace -PpreDexEnable=false --max-workers 2 --no-daemon - -jobs: - debug: - name: Build debug APKs ER - runs-on: ubuntu-latest - if: github.ref != 'refs/heads/main' - strategy: - fail-fast: false - matrix: - target: [ Gplay, Fdroid ] - # Allow all jobs on develop. Just one per PR. - concurrency: - group: ${{ github.ref == 'refs/heads/develop' && format('elementr-{0}-{1}', matrix.target, github.sha) || format('build-er-debug-{0}-{1}', matrix.target, github.ref) }} - cancel-in-progress: true - steps: - - uses: actions/checkout@v3 - - uses: actions/cache@v3 - with: - path: | - ~/.gradle/caches - ~/.gradle/wrapper - key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} - restore-keys: | - ${{ runner.os }}-gradle- - - name: Assemble ${{ matrix.target }} debug apk - run: ./gradlew assemble${{ matrix.target }}RustCryptoDebug $CI_GRADLE_ARG_PROPERTIES diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 93d63af5d3..5ad3adc7d2 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -35,7 +35,7 @@ jobs: yes n | towncrier build --version nightly - name: Build and upload Gplay Nightly APK run: | - ./gradlew assembleGplayRustCryptoNightly appDistributionUploadGplayRustCryptoNightly $CI_GRADLE_ARG_PROPERTIES + ./gradlew assembleGplayNightly appDistributionUploadGplayNightly $CI_GRADLE_ARG_PROPERTIES env: ELEMENT_ANDROID_NIGHTLY_KEYID: ${{ secrets.ELEMENT_ANDROID_NIGHTLY_KEYID }} ELEMENT_ANDROID_NIGHTLY_KEYPASSWORD: ${{ secrets.ELEMENT_ANDROID_NIGHTLY_KEYPASSWORD }} diff --git a/.github/workflows/nightly_er.yml b/.github/workflows/nightly_er.yml deleted file mode 100644 index 7efa900b06..0000000000 --- a/.github/workflows/nightly_er.yml +++ /dev/null @@ -1,46 +0,0 @@ -name: Build and release Element R nightly APK - -on: - schedule: - # Every nights at 4 - - cron: "0 4 * * *" - -env: - GRADLE_OPTS: -Dorg.gradle.jvmargs="-Xmx3072m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError -XX:MaxMetaspaceSize=1g" -Dkotlin.daemon.jvm.options="-Xmx2560m" -Dkotlin.incremental=false - CI_GRADLE_ARG_PROPERTIES: --stacktrace -PpreDexEnable=false --max-workers 2 --no-daemon - -jobs: - nightly: - name: Build and publish ER nightly Gplay APK to Firebase - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - name: Set up Python 3.8 - uses: actions/setup-python@v4 - with: - python-version: 3.8 - - uses: actions/cache@v3 - with: - path: | - ~/.gradle/caches - ~/.gradle/wrapper - key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} - restore-keys: | - ${{ runner.os }}-gradle- - - name: Install towncrier - run: | - python3 -m pip install towncrier - - name: Prepare changelog file - run: | - mv towncrier.toml towncrier.toml.bak - sed 's/CHANGES\.md/CHANGES_NIGHTLY\.md/' towncrier.toml.bak > towncrier.toml - rm towncrier.toml.bak - yes n | towncrier build --version nightly - - name: Build and upload Gplay Nightly ER APK - run: | - ./gradlew assembleGplayRustCryptoNightly appDistributionUploadGplayRustCryptoNightly $CI_GRADLE_ARG_PROPERTIES - env: - ELEMENT_ANDROID_NIGHTLY_KEYID: ${{ secrets.ELEMENT_ANDROID_NIGHTLY_KEYID }} - ELEMENT_ANDROID_NIGHTLY_KEYPASSWORD: ${{ secrets.ELEMENT_ANDROID_NIGHTLY_KEYPASSWORD }} - ELEMENT_ANDROID_NIGHTLY_STOREPASSWORD: ${{ secrets.ELEMENT_ANDROID_NIGHTLY_STOREPASSWORD }} - FIREBASE_TOKEN: ${{ secrets.ELEMENT_R_NIGHTLY_FIREBASE_TOKEN }} diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml index b007073106..4b12594a68 100644 --- a/.github/workflows/quality.yml +++ b/.github/workflows/quality.yml @@ -49,10 +49,8 @@ jobs: - name: Run lint # Not always, if ktlint or detekt fail, avoid running the long lint check. run: | - ./gradlew vector-app:lintGplayKotlinCryptoRelease $CI_GRADLE_ARG_PROPERTIES - ./gradlew vector-app:lintFdroidKotlinCryptoRelease $CI_GRADLE_ARG_PROPERTIES - ./gradlew vector-app:lintGplayRustCryptoRelease $CI_GRADLE_ARG_PROPERTIES - ./gradlew vector-app:lintFdroidRustCryptoRelease $CI_GRADLE_ARG_PROPERTIES + ./gradlew vector-app:lintGplayRelease $CI_GRADLE_ARG_PROPERTIES + ./gradlew vector-app:lintFdroidRelease $CI_GRADLE_ARG_PROPERTIES - name: Upload reports if: always() uses: actions/upload-artifact@v3 diff --git a/.github/workflows/tests-rust.yml b/.github/workflows/tests-rust.yml deleted file mode 100644 index 803b0a08ab..0000000000 --- a/.github/workflows/tests-rust.yml +++ /dev/null @@ -1,102 +0,0 @@ -name: Test - -on: - pull_request: { } - push: - branches: [ main, develop ] - paths-ignore: - - '.github/**' - -# Enrich gradle.properties for CI/CD -env: - GRADLE_OPTS: -Dorg.gradle.jvmargs="-Xmx5g -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError" -Dkotlin.daemon.jvm.options="-Xmx3g" -Dkotlin.incremental=false - CI_GRADLE_ARG_PROPERTIES: --stacktrace -PpreDexEnable=false --max-workers 4 --no-daemon - -jobs: - tests: - name: Runs all tests with rust crypto - runs-on: buildjet-4vcpu-ubuntu-2204 - timeout-minutes: 90 # We might need to increase it if the time for tests grows - strategy: - matrix: - api-level: [28] - # Allow all jobs on main and develop. Just one per PR. - concurrency: - group: ${{ github.ref == 'refs/heads/main' && format('unit-tests-main-rust-{0}', github.sha) || github.ref == 'refs/heads/develop' && format('unit-tests-develop-rust-{0}', github.sha) || format('unit-tests-rust-{0}', github.ref) }} - cancel-in-progress: true - steps: - - uses: actions/checkout@v3 - with: - lfs: true - fetch-depth: 0 - - uses: actions/setup-java@v3 - with: - distribution: 'adopt' - java-version: '11' - - uses: gradle/gradle-build-action@v2 - with: - cache-read-only: ${{ github.ref != 'refs/heads/develop' }} - gradle-home-cache-cleanup: ${{ github.ref == 'refs/heads/develop' }} - -# - name: Run screenshot tests -# run: ./gradlew verifyScreenshots $CI_GRADLE_ARG_PROPERTIES - -# - name: Archive Screenshot Results on Error -# if: failure() -# uses: actions/upload-artifact@v3 -# with: -# name: screenshot-results -# path: | -# **/out/failures/ -# **/build/reports/tests/*UnitTest/ - - - uses: actions/setup-python@v4 - with: - python-version: 3.8 - - uses: michaelkaye/setup-matrix-synapse@v1.0.4 - with: - uploadLogs: true - httpPort: 8080 - disableRateLimiting: true - public_baseurl: "http://10.0.2.2:8080/" - - - name: Run all the codecoverage tests at once - uses: reactivecircus/android-emulator-runner@v2 - # continue-on-error: true - with: - api-level: ${{ matrix.api-level }} - arch: x86 - profile: Nexus 5X - target: playstore - force-avd-creation: false - emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none - disable-animations: true - # emulator-build: 7425822 - script: | - ./gradlew gatherGplayRustCryptoDebugStringTemplates $CI_GRADLE_ARG_PROPERTIES - ./gradlew instrumentationTestsRustWithCoverage $CI_GRADLE_ARG_PROPERTIES - ./gradlew generateCoverageReport $CI_GRADLE_ARG_PROPERTIES - - - name: Upload Rust Integration Test Report Log - uses: actions/upload-artifact@v3 - if: always() - with: - name: integration-test-rust-error-results - path: | - */build/outputs/androidTest-results/connected/ - */build/reports/androidTests/connected/ - - # For now ignore sonar -# - name: Publish results to Sonar -# env: -# GITHUB_TOKEN: ${{ secrets.SONARQUBE_GITHUB_API_TOKEN }} # Needed to get PR information, if any -# SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} -# ORG_GRADLE_PROJECT_SONAR_LOGIN: ${{ secrets.SONAR_TOKEN }} -# if: ${{ always() && env.GITHUB_TOKEN != '' && env.SONAR_TOKEN != '' && env.ORG_GRADLE_PROJECT_SONAR_LOGIN != '' }} -# run: ./gradlew sonar $CI_GRADLE_ARG_PROPERTIES - - - name: Format unit test results - if: always() - run: python3 ./tools/ci/render_test_output.py unit ./**/build/test-results/**/*.xml - - diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 7af7ce2a36..6ee85168af 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -73,7 +73,7 @@ jobs: disable-animations: true # emulator-build: 7425822 script: | - ./gradlew gatherGplayKotlinCryptoDebugStringTemplates $CI_GRADLE_ARG_PROPERTIES + ./gradlew gatherGplayDebugStringTemplates $CI_GRADLE_ARG_PROPERTIES ./gradlew unitTestsWithCoverage $CI_GRADLE_ARG_PROPERTIES ./gradlew instrumentationTestsWithCoverage $CI_GRADLE_ARG_PROPERTIES ./gradlew generateCoverageReport $CI_GRADLE_ARG_PROPERTIES diff --git a/build.gradle b/build.gradle index dae89f7aff..a7512ea992 100644 --- a/build.gradle +++ b/build.gradle @@ -312,7 +312,7 @@ tasks.register("recordScreenshots", GradleBuild) { tasks.register("verifyScreenshots", GradleBuild) { startParameter.projectProperties.screenshot = "" - tasks = [':vector:verifyPaparazziRustCryptoDebug'] + tasks = [':vector:verifyPaparazziDebug'] } ext.initScreenshotTests = { project -> diff --git a/coverage.gradle b/coverage.gradle index dc60b1b273..421c500728 100644 --- a/coverage.gradle +++ b/coverage.gradle @@ -87,11 +87,5 @@ task unitTestsWithCoverage(type: GradleBuild) { task instrumentationTestsWithCoverage(type: GradleBuild) { startParameter.projectProperties.coverage = "true" startParameter.projectProperties['android.testInstrumentationRunnerArguments.notPackage'] = 'im.vector.app.ui' - tasks = [':vector-app:connectedGplayKotlinCryptoDebugAndroidTest', ':vector:connectedKotlinCryptoDebugAndroidTest', 'matrix-sdk-android:connectedKotlinCryptoDebugAndroidTest'] -} - -task instrumentationTestsRustWithCoverage(type: GradleBuild) { - startParameter.projectProperties.coverage = "true" - startParameter.projectProperties['android.testInstrumentationRunnerArguments.notPackage'] = 'im.vector.app.ui' - tasks = [':vector-app:connectedGplayRustCryptoDebugAndroidTest', ':vector:connectedRustCryptoDebugAndroidTest', 'matrix-sdk-android:connectedRustCryptoDebugAndroidTest'] + tasks = [':vector-app:connectedGplayDebugAndroidTest', ':vector:connectedDebugAndroidTest', 'matrix-sdk-android:connectedDebugAndroidTest'] } diff --git a/docs/nightly_build.md b/docs/nightly_build.md index ea515e90eb..cdbda5c64f 100644 --- a/docs/nightly_build.md +++ b/docs/nightly_build.md @@ -48,7 +48,7 @@ mv towncrier.toml towncrier.toml.bak sed 's/CHANGES\.md/CHANGES_NIGHTLY\.md/' towncrier.toml.bak > towncrier.toml rm towncrier.toml.bak yes n | towncrier build --version nightly -./gradlew assembleGplayRustCryptoNightly appDistributionUploadRustKotlinCryptoNightly $CI_GRADLE_ARG_PROPERTIES +./gradlew assembleGplayNightly appDistributionUploadNightly $CI_GRADLE_ARG_PROPERTIES ``` Then you can reset the change on the codebase. diff --git a/flavor.gradle b/flavor.gradle deleted file mode 100644 index 946040e4ed..0000000000 --- a/flavor.gradle +++ /dev/null @@ -1,20 +0,0 @@ -android { - - flavorDimensions "crypto" - - productFlavors { - kotlinCrypto { - dimension "crypto" - // versionName "${versionMajor}.${versionMinor}.${versionPatch}${getFdroidVersionSuffix()}" -// buildConfigField "String", "SHORT_FLAVOR_DESCRIPTION", "\"JC\"" -// buildConfigField "String", "FLAVOR_DESCRIPTION", "\"KotlinCrypto\"" - } - rustCrypto { - dimension "crypto" - isDefault = true -// // versionName "${versionMajor}.${versionMinor}.${versionPatch}${getFdroidVersionSuffix()}" -// buildConfigField "String", "SHORT_FLAVOR_DESCRIPTION", "\"RC\"" -// buildConfigField "String", "FLAVOR_DESCRIPTION", "\"RustCrypto\"" - } - } -} diff --git a/matrix-sdk-android-flow/build.gradle b/matrix-sdk-android-flow/build.gradle index bcc070f23a..8b0fe5003d 100644 --- a/matrix-sdk-android-flow/build.gradle +++ b/matrix-sdk-android-flow/build.gradle @@ -3,7 +3,6 @@ plugins { id 'com.android.library' id 'org.jetbrains.kotlin.android' } -apply from: '../flavor.gradle' android { namespace "org.matrix.android.sdk.flow" diff --git a/matrix-sdk-android/build.gradle b/matrix-sdk-android/build.gradle index 63fb13ead8..26e96b053f 100644 --- a/matrix-sdk-android/build.gradle +++ b/matrix-sdk-android/build.gradle @@ -41,7 +41,6 @@ dokkaHtml { } } } -apply from: '../flavor.gradle' android { namespace "org.matrix.android.sdk" @@ -158,7 +157,7 @@ dependencies { // implementation libs.androidx.appCompat implementation libs.androidx.core - rustCryptoImplementation libs.androidx.lifecycleLivedata + implementation libs.androidx.lifecycleLivedata // Lifecycle implementation libs.androidx.lifecycleCommon @@ -216,8 +215,8 @@ dependencies { implementation libs.google.phonenumber - rustCryptoImplementation("org.matrix.rustcomponents:crypto-android:0.3.15") -// rustCryptoApi project(":library:rustCrypto") + implementation("org.matrix.rustcomponents:crypto-android:0.3.15") +// api project(":library:rustCrypto") testImplementation libs.tests.junit // Note: version sticks to 1.9.2 due to https://github.com/mockk/mockk/issues/281 diff --git a/matrix-sdk-android/src/androidTestRustCrypto/java/org/matrix/android/sdk/internal/crypto/store/migration/DynamicElementAndroidToElementRMigrationTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/store/migration/DynamicElementAndroidToElementRMigrationTest.kt similarity index 100% rename from matrix-sdk-android/src/androidTestRustCrypto/java/org/matrix/android/sdk/internal/crypto/store/migration/DynamicElementAndroidToElementRMigrationTest.kt rename to matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/store/migration/DynamicElementAndroidToElementRMigrationTest.kt diff --git a/matrix-sdk-android/src/androidTestRustCrypto/java/org/matrix/android/sdk/internal/database/CryptoSanityMigrationTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/database/CryptoSanityMigrationTest.kt similarity index 100% rename from matrix-sdk-android/src/androidTestRustCrypto/java/org/matrix/android/sdk/internal/database/CryptoSanityMigrationTest.kt rename to matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/database/CryptoSanityMigrationTest.kt diff --git a/matrix-sdk-android/src/androidTestKotlinCrypto/java/org/matrix/android/sdk/internal/crypto/CryptoStoreHelper.kt b/matrix-sdk-android/src/androidTestKotlinCrypto/java/org/matrix/android/sdk/internal/crypto/CryptoStoreHelper.kt deleted file mode 100644 index 48cfbebe5b..0000000000 --- a/matrix-sdk-android/src/androidTestKotlinCrypto/java/org/matrix/android/sdk/internal/crypto/CryptoStoreHelper.kt +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright 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. - */ - -package org.matrix.android.sdk.internal.crypto - -import io.realm.RealmConfiguration -import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore -import org.matrix.android.sdk.internal.crypto.store.db.RealmCryptoStore -import org.matrix.android.sdk.internal.crypto.store.db.RealmCryptoStoreModule -import org.matrix.android.sdk.internal.crypto.store.db.mapper.CrossSigningKeysMapper -import org.matrix.android.sdk.internal.crypto.store.db.mapper.MyDeviceLastSeenInfoEntityMapper -import org.matrix.android.sdk.internal.di.MoshiProvider -import org.matrix.android.sdk.internal.util.time.DefaultClock -import kotlin.random.Random - -internal class CryptoStoreHelper { - - fun createStore(): IMXCryptoStore { - return RealmCryptoStore( - realmConfiguration = RealmConfiguration.Builder() - .name("test.realm") - .modules(RealmCryptoStoreModule()) - .build(), - crossSigningKeysMapper = CrossSigningKeysMapper(MoshiProvider.providesMoshi()), - userId = "userId_" + Random.nextInt(), - deviceId = "deviceId_sample", - clock = DefaultClock(), - myDeviceLastSeenInfoEntityMapper = MyDeviceLastSeenInfoEntityMapper() - ) - } -} diff --git a/matrix-sdk-android/src/androidTestKotlinCrypto/java/org/matrix/android/sdk/internal/crypto/CryptoStoreTest.kt b/matrix-sdk-android/src/androidTestKotlinCrypto/java/org/matrix/android/sdk/internal/crypto/CryptoStoreTest.kt deleted file mode 100644 index dbc6929e34..0000000000 --- a/matrix-sdk-android/src/androidTestKotlinCrypto/java/org/matrix/android/sdk/internal/crypto/CryptoStoreTest.kt +++ /dev/null @@ -1,142 +0,0 @@ -/* - * Copyright 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. - */ - -package org.matrix.android.sdk.internal.crypto - -import androidx.test.ext.junit.runners.AndroidJUnit4 -import io.realm.Realm -import org.junit.Assert.assertEquals -import org.junit.Assert.assertNotEquals -import org.junit.Assert.assertNull -import org.junit.Before -import org.junit.Ignore -import org.junit.Rule -import org.junit.Test -import org.junit.runner.RunWith -import org.matrix.android.sdk.InstrumentedTest -import org.matrix.android.sdk.common.RetryTestRule -import org.matrix.android.sdk.internal.crypto.model.OlmSessionWrapper -import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore -import org.matrix.android.sdk.internal.util.time.DefaultClock -import org.matrix.olm.OlmAccount -import org.matrix.olm.OlmManager -import org.matrix.olm.OlmSession - -private const val DUMMY_DEVICE_KEY = "DeviceKey" - -@RunWith(AndroidJUnit4::class) -@Ignore -class CryptoStoreTest : InstrumentedTest { - - @get:Rule val rule = RetryTestRule(3) - - private val cryptoStoreHelper = CryptoStoreHelper() - private val clock = DefaultClock() - - @Before - fun setup() { - Realm.init(context()) - } - -// @Test -// fun test_metadata_realm_ok() { -// val cryptoStore: IMXCryptoStore = cryptoStoreHelper.createStore() -// -// assertFalse(cryptoStore.hasData()) -// -// cryptoStore.open() -// -// assertEquals("deviceId_sample", cryptoStore.getDeviceId()) -// -// assertTrue(cryptoStore.hasData()) -// -// // Cleanup -// cryptoStore.close() -// cryptoStore.deleteStore() -// } - - @Test - fun test_lastSessionUsed() { - // Ensure Olm is initialized - OlmManager() - - val cryptoStore: IMXCryptoStore = cryptoStoreHelper.createStore() - - assertNull(cryptoStore.getLastUsedSessionId(DUMMY_DEVICE_KEY)) - - val olmAccount1 = OlmAccount().apply { - generateOneTimeKeys(1) - } - - val olmSession1 = OlmSession().apply { - initOutboundSession( - olmAccount1, - olmAccount1.identityKeys()[OlmAccount.JSON_KEY_IDENTITY_KEY], - olmAccount1.oneTimeKeys()[OlmAccount.JSON_KEY_ONE_TIME_KEY]?.values?.first() - ) - } - - val sessionId1 = olmSession1.sessionIdentifier() - val olmSessionWrapper1 = OlmSessionWrapper(olmSession1) - - cryptoStore.storeSession(olmSessionWrapper1, DUMMY_DEVICE_KEY) - - assertEquals(sessionId1, cryptoStore.getLastUsedSessionId(DUMMY_DEVICE_KEY)) - - val olmAccount2 = OlmAccount().apply { - generateOneTimeKeys(1) - } - - val olmSession2 = OlmSession().apply { - initOutboundSession( - olmAccount2, - olmAccount2.identityKeys()[OlmAccount.JSON_KEY_IDENTITY_KEY], - olmAccount2.oneTimeKeys()[OlmAccount.JSON_KEY_ONE_TIME_KEY]?.values?.first() - ) - } - - val sessionId2 = olmSession2.sessionIdentifier() - val olmSessionWrapper2 = OlmSessionWrapper(olmSession2) - - cryptoStore.storeSession(olmSessionWrapper2, DUMMY_DEVICE_KEY) - - // Ensure sessionIds are distinct - assertNotEquals(sessionId1, sessionId2) - - // Note: we cannot be sure what will be the result of getLastUsedSessionId() here - - olmSessionWrapper2.onMessageReceived(clock.epochMillis()) - cryptoStore.storeSession(olmSessionWrapper2, DUMMY_DEVICE_KEY) - - // sessionId2 is returned now - assertEquals(sessionId2, cryptoStore.getLastUsedSessionId(DUMMY_DEVICE_KEY)) - - Thread.sleep(2) - - olmSessionWrapper1.onMessageReceived(clock.epochMillis()) - cryptoStore.storeSession(olmSessionWrapper1, DUMMY_DEVICE_KEY) - - // sessionId1 is returned now - assertEquals(sessionId1, cryptoStore.getLastUsedSessionId(DUMMY_DEVICE_KEY)) - - // Cleanup - olmSession1.releaseSession() - olmSession2.releaseSession() - - olmAccount1.releaseAccount() - olmAccount2.releaseAccount() - } -} diff --git a/matrix-sdk-android/src/androidTestKotlinCrypto/java/org/matrix/android/sdk/internal/crypto/PreShareKeysTest.kt b/matrix-sdk-android/src/androidTestKotlinCrypto/java/org/matrix/android/sdk/internal/crypto/PreShareKeysTest.kt deleted file mode 100644 index eda13e31ec..0000000000 --- a/matrix-sdk-android/src/androidTestKotlinCrypto/java/org/matrix/android/sdk/internal/crypto/PreShareKeysTest.kt +++ /dev/null @@ -1,92 +0,0 @@ -/* - * Copyright 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. - */ - -package org.matrix.android.sdk.internal.crypto - -import android.util.Log -import androidx.test.ext.junit.runners.AndroidJUnit4 -import org.junit.Assert.assertEquals -import org.junit.FixMethodOrder -import org.junit.Test -import org.junit.runner.RunWith -import org.junit.runners.MethodSorters -import org.matrix.android.sdk.InstrumentedTest -import org.matrix.android.sdk.api.session.events.model.EventType -import org.matrix.android.sdk.api.session.getRoom -import org.matrix.android.sdk.api.session.room.getTimelineEvent -import org.matrix.android.sdk.common.CommonTestHelper.Companion.runCryptoTest - -@RunWith(AndroidJUnit4::class) -@FixMethodOrder(MethodSorters.JVM) -class PreShareKeysTest : InstrumentedTest { - - @Test - fun ensure_outbound_session_happy_path() = runCryptoTest(context()) { cryptoTestHelper, testHelper -> - val testData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom() - val e2eRoomID = testData.roomId - val aliceSession = testData.firstSession - val bobSession = testData.secondSession!! - - // clear any outbound session - aliceSession.cryptoService().discardOutboundSession(e2eRoomID) - - val preShareCount = bobSession.cryptoService().keysBackupService().getTotalNumbersOfKeys() - - assertEquals("Bob should not have receive any key from alice at this point", 0, preShareCount) - Log.d("#E2E", "Room Key Received from alice $preShareCount") - - // Force presharing of new outbound key - aliceSession.cryptoService().prepareToEncrypt(e2eRoomID) - - testHelper.retryPeriodically { - val newKeysCount = bobSession.cryptoService().keysBackupService().getTotalNumbersOfKeys() - newKeysCount > preShareCount - } - - val newKeysCount = bobSession.cryptoService().keysBackupService().getTotalNumbersOfKeys() - -// val aliceCryptoStore = (aliceSession.cryptoService() as DefaultCryptoService).cryptoStoreForTesting -// val aliceOutboundSessionInRoom = aliceCryptoStore.getCurrentOutboundGroupSessionForRoom(e2eRoomID)!!.outboundGroupSession.sessionIdentifier() -// -// val bobCryptoStore = (bobSession.cryptoService() as DefaultCryptoService).cryptoStoreForTesting -// val aliceDeviceBobPov = bobCryptoStore.getUserDevice(aliceSession.myUserId, aliceSession.sessionParams.deviceId)!! -// val bobInboundForAlice = bobCryptoStore.getInboundGroupSession(aliceOutboundSessionInRoom, aliceDeviceBobPov.identityKey()!!) -// assertNotNull("Bob should have received and decrypted a room key event from alice", bobInboundForAlice) -// assertEquals("Wrong room", e2eRoomID, bobInboundForAlice!!.roomId) - -// val megolmSessionId = bobInboundForAlice.session.sessionIdentifier() -// -// assertEquals("Wrong session", aliceOutboundSessionInRoom, megolmSessionId) - -// val sharedIndex = aliceSession.cryptoService().getSharedWithInfo(e2eRoomID, megolmSessionId) -// .getObject(bobSession.myUserId, bobSession.sessionParams.deviceId) -// -// assertEquals("The session received by bob should match what alice sent", 0, sharedIndex) - - // Just send a real message as test - val sentEventId = testHelper.sendMessageInRoom(aliceSession.getRoom(e2eRoomID)!!, "Allo") - - val sentEvent = aliceSession.getRoom(e2eRoomID)!!.getTimelineEvent(sentEventId)!! - -// assertEquals("Unexpected megolm session", megolmSessionId, sentEvent.root.content.toModel()?.sessionId) - testHelper.retryPeriodically { - bobSession.getRoom(e2eRoomID)?.getTimelineEvent(sentEvent.eventId)?.root?.getClearType() == EventType.MESSAGE - } - - // check that no additional key was shared - assertEquals(newKeysCount, bobSession.cryptoService().keysBackupService().getTotalNumbersOfKeys()) - } -} diff --git a/matrix-sdk-android/src/androidTestKotlinCrypto/java/org/matrix/android/sdk/internal/crypto/UnwedgingTest.kt b/matrix-sdk-android/src/androidTestKotlinCrypto/java/org/matrix/android/sdk/internal/crypto/UnwedgingTest.kt deleted file mode 100644 index f32e0aa4e5..0000000000 --- a/matrix-sdk-android/src/androidTestKotlinCrypto/java/org/matrix/android/sdk/internal/crypto/UnwedgingTest.kt +++ /dev/null @@ -1,225 +0,0 @@ -/* - * Copyright 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. - */ - -package org.matrix.android.sdk.internal.crypto - -import androidx.test.ext.junit.runners.AndroidJUnit4 -import kotlinx.coroutines.suspendCancellableCoroutine -import org.amshove.kluent.shouldBeEqualTo -import org.junit.Assert -import org.junit.Before -import org.junit.FixMethodOrder -import org.junit.Test -import org.junit.runner.RunWith -import org.junit.runners.MethodSorters -import org.matrix.android.sdk.InstrumentedTest -import org.matrix.android.sdk.api.auth.UIABaseAuth -import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor -import org.matrix.android.sdk.api.auth.UserPasswordAuth -import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse -import org.matrix.android.sdk.api.crypto.MXCryptoConfig -import org.matrix.android.sdk.api.extensions.tryOrNull -import org.matrix.android.sdk.api.session.crypto.MXCryptoError -import org.matrix.android.sdk.api.session.events.model.EventType -import org.matrix.android.sdk.api.session.events.model.content.EncryptedEventContent -import org.matrix.android.sdk.api.session.events.model.toModel -import org.matrix.android.sdk.api.session.getRoom -import org.matrix.android.sdk.api.session.room.timeline.Timeline -import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent -import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings -import org.matrix.android.sdk.common.CommonTestHelper.Companion.runCryptoTest -import org.matrix.android.sdk.common.TestConstants -import org.matrix.android.sdk.internal.crypto.model.OlmSessionWrapper -import org.matrix.android.sdk.internal.crypto.store.db.deserializeFromRealm -import org.matrix.android.sdk.internal.crypto.store.db.serializeForRealm -import org.matrix.olm.OlmSession -import timber.log.Timber -import kotlin.coroutines.Continuation -import kotlin.coroutines.resume - -/** - * Ref: - * - https://github.com/matrix-org/matrix-doc/pull/1719 - * - https://matrix.org/docs/spec/client_server/latest#recovering-from-undecryptable-messages - * - https://github.com/matrix-org/matrix-js-sdk/pull/780 - * - https://github.com/matrix-org/matrix-ios-sdk/pull/778 - * - https://github.com/matrix-org/matrix-ios-sdk/pull/784 - */ -@RunWith(AndroidJUnit4::class) -@FixMethodOrder(MethodSorters.JVM) -class UnwedgingTest : InstrumentedTest { - - private lateinit var messagesReceivedByBob: List - - @Before - fun init() { - messagesReceivedByBob = emptyList() - } - - /** - * - Alice & Bob in a e2e room - * - Alice sends a 1st message with a 1st megolm session - * - Store the olm session between A&B devices - * - Alice sends a 2nd message with a 2nd megolm session - * - Simulate Alice using a backup of her OS and make her crypto state like after the first message - * - Alice sends a 3rd message with a 3rd megolm session but a wedged olm session - * - * What Bob must see: - * -> No issue with the 2 first messages - * -> The third event must fail to decrypt at first because Bob the olm session is wedged - * -> This is automatically fixed after SDKs restarted the olm session - */ - @Test - fun testUnwedging() = runCryptoTest( - context(), - cryptoConfig = MXCryptoConfig(limitRoomKeyRequestsToMyDevices = false) - ) { cryptoTestHelper, testHelper -> - val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom() - - val aliceSession = cryptoTestData.firstSession - val aliceRoomId = cryptoTestData.roomId - val bobSession = cryptoTestData.secondSession!! - - val aliceCryptoStore = (aliceSession.cryptoService() as DefaultCryptoService).cryptoStoreForTesting - val olmDevice = (aliceSession.cryptoService() as DefaultCryptoService).olmDeviceForTest - - val roomFromBobPOV = bobSession.getRoom(aliceRoomId)!! - val roomFromAlicePOV = aliceSession.getRoom(aliceRoomId)!! - - val bobTimeline = roomFromBobPOV.timelineService().createTimeline(null, TimelineSettings(20)) - bobTimeline.start() - - messagesReceivedByBob = emptyList() - - // - Alice sends a 1st message with a 1st megolm session - roomFromAlicePOV.sendService().sendTextMessage("First message") - - // Wait for the message to be received by Bob - messagesReceivedByBob = bobTimeline.waitForMessages(expectedCount = 1) - - messagesReceivedByBob.size shouldBeEqualTo 1 - val firstMessageSession = messagesReceivedByBob[0].root.content.toModel()!!.sessionId!! - - // - Store the olm session between A&B devices - // Let us pickle our session with bob here so we can later unpickle it - // and wedge our session. - val sessionIdsForBob = aliceCryptoStore.getDeviceSessionIds(bobSession.cryptoService().getMyCryptoDevice().identityKey()!!) - sessionIdsForBob!!.size shouldBeEqualTo 1 - val olmSession = aliceCryptoStore.getDeviceSession(sessionIdsForBob.first(), bobSession.cryptoService().getMyCryptoDevice().identityKey()!!)!! - - val oldSession = serializeForRealm(olmSession.olmSession) - - aliceSession.cryptoService().discardOutboundSession(roomFromAlicePOV.roomId) - - messagesReceivedByBob = emptyList() - Timber.i("## CRYPTO | testUnwedging: Alice sends a 2nd message with a 2nd megolm session") - // - Alice sends a 2nd message with a 2nd megolm session - roomFromAlicePOV.sendService().sendTextMessage("Second message") - - // Wait for the message to be received by Bob - messagesReceivedByBob = bobTimeline.waitForMessages(expectedCount = 2) - - messagesReceivedByBob.size shouldBeEqualTo 2 - // Session should have changed - val secondMessageSession = messagesReceivedByBob[0].root.content.toModel()!!.sessionId!! - Assert.assertNotEquals(firstMessageSession, secondMessageSession) - - // Let us wedge the session now. Set crypto state like after the first message - Timber.i("## CRYPTO | testUnwedging: wedge the session now. Set crypto state like after the first message") - - aliceCryptoStore.storeSession( - OlmSessionWrapper(deserializeFromRealm(oldSession)!!), - bobSession.cryptoService().getMyCryptoDevice().identityKey()!! - ) - olmDevice.clearOlmSessionCache() - - // Force new session, and key share - aliceSession.cryptoService().discardOutboundSession(roomFromAlicePOV.roomId) - - Timber.i("## CRYPTO | testUnwedging: Alice sends a 3rd message with a 3rd megolm session but a wedged olm session") - // - Alice sends a 3rd message with a 3rd megolm session but a wedged olm session - roomFromAlicePOV.sendService().sendTextMessage("Third message") - // Bob should not be able to decrypt, because the session key could not be sent - // Wait for the message to be received by Bob - messagesReceivedByBob = bobTimeline.waitForMessages(expectedCount = 3) - - messagesReceivedByBob.size shouldBeEqualTo 3 - - val thirdMessageSession = messagesReceivedByBob[0].root.content.toModel()!!.sessionId!! - Timber.i("## CRYPTO | testUnwedging: third message session ID $thirdMessageSession") - Assert.assertNotEquals(secondMessageSession, thirdMessageSession) - - Assert.assertEquals(EventType.ENCRYPTED, messagesReceivedByBob[0].root.getClearType()) - Assert.assertEquals(EventType.MESSAGE, messagesReceivedByBob[1].root.getClearType()) - Assert.assertEquals(EventType.MESSAGE, messagesReceivedByBob[2].root.getClearType()) - // Bob Should not be able to decrypt last message, because session could not be sent as the olm channel was wedged - - Assert.assertTrue(messagesReceivedByBob[0].root.mCryptoError == MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID) - - // It's a trick to force key request on fail to decrypt - bobSession.cryptoService().crossSigningService() - .initializeCrossSigning( - object : UserInteractiveAuthInterceptor { - override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation) { - promise.resume( - UserPasswordAuth( - user = bobSession.myUserId, - password = TestConstants.PASSWORD, - session = flowResponse.session - ) - ) - } - }) - - // Wait until we received back the key - testHelper.retryPeriodically { - // we should get back the key and be able to decrypt - val result = tryOrNull { - bobSession.cryptoService().decryptEvent(messagesReceivedByBob[0].root, "") - } - Timber.i("## CRYPTO | testUnwedging: decrypt result ${result?.clearEvent}") - result != null - } - - bobTimeline.dispose() - } -} - -private suspend fun Timeline.waitForMessages(expectedCount: Int): List { - return suspendCancellableCoroutine { continuation -> - val listener = object : Timeline.Listener { - override fun onTimelineFailure(throwable: Throwable) { - // noop - } - - override fun onNewTimelineEvents(eventIds: List) { - // noop - } - - override fun onTimelineUpdated(snapshot: List) { - val messagesReceived = snapshot.filter { it.root.type == EventType.ENCRYPTED } - - if (messagesReceived.size == expectedCount) { - removeListener(this) - continuation.resume(messagesReceived) - } - } - } - - addListener(listener) - continuation.invokeOnCancellation { removeListener(listener) } - } -} diff --git a/matrix-sdk-android/src/androidTestKotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/SASTest.kt b/matrix-sdk-android/src/androidTestKotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/SASTest.kt deleted file mode 100644 index b1969e13e9..0000000000 --- a/matrix-sdk-android/src/androidTestKotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/SASTest.kt +++ /dev/null @@ -1,609 +0,0 @@ -/* - * Copyright 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. - */ - -package org.matrix.android.sdk.internal.crypto.verification - -import android.util.Log -import androidx.test.ext.junit.runners.AndroidJUnit4 -import kotlinx.coroutines.CompletableDeferred -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.SupervisorJob -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach -import org.amshove.kluent.internal.assertEquals -import org.junit.Assert.assertNotNull -import org.junit.Assert.assertNull -import org.junit.Assert.assertTrue -import org.junit.FixMethodOrder -import org.junit.Test -import org.junit.runner.RunWith -import org.junit.runners.MethodSorters -import org.matrix.android.sdk.InstrumentedTest -import org.matrix.android.sdk.api.session.crypto.verification.CancelCode -import org.matrix.android.sdk.api.session.crypto.verification.EVerificationState -import org.matrix.android.sdk.api.session.crypto.verification.SasTransactionState -import org.matrix.android.sdk.api.session.crypto.verification.SasVerificationTransaction -import org.matrix.android.sdk.api.session.crypto.verification.VerificationMethod -import org.matrix.android.sdk.api.session.crypto.verification.dbgState -import org.matrix.android.sdk.api.session.crypto.verification.getTransaction -import org.matrix.android.sdk.common.CommonTestHelper.Companion.runCryptoTest - -@RunWith(AndroidJUnit4::class) -@FixMethodOrder(MethodSorters.NAME_ASCENDING) -class SASTest : InstrumentedTest { - - val scope = CoroutineScope(SupervisorJob()) - - @Test - fun test_aliceStartThenAliceCancel() = runCryptoTest(context()) { cryptoTestHelper, testHelper -> - - Log.d("#E2E", "verification: doE2ETestWithAliceAndBobInARoom") - val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom() - Log.d("#E2E", "verification: initializeCrossSigning") - cryptoTestData.initializeCrossSigning(cryptoTestHelper) - val aliceSession = cryptoTestData.firstSession - val bobSession = cryptoTestData.secondSession - - val aliceVerificationService = aliceSession.cryptoService().verificationService() - val bobVerificationService = bobSession!!.cryptoService().verificationService() - - Log.d("#E2E", "verification: requestVerificationAndWaitForReadyState") - val txId = SasVerificationTestHelper(testHelper) - .requestVerificationAndWaitForReadyState(scope, cryptoTestData, listOf(VerificationMethod.SAS)) - - Log.d("#E2E", "verification: startKeyVerification") - aliceVerificationService.startKeyVerification( - VerificationMethod.SAS, - bobSession.myUserId, - txId - ) - - Log.d("#E2E", "verification: ensure bob has received start") - testHelper.retryWithBackoff { - Log.d("#E2E", "verification: ${bobVerificationService.getExistingVerificationRequest(aliceSession.myUserId, txId)?.state}") - bobVerificationService.getExistingVerificationRequest(aliceSession.myUserId, txId)?.state == EVerificationState.Started - } - - val bobKeyTx = bobVerificationService.getExistingTransaction(aliceSession.myUserId, txId) - - assertNotNull("Bob should have started verif transaction", bobKeyTx) - assertTrue(bobKeyTx is SasVerificationTransaction) - - val aliceKeyTx = aliceVerificationService.getExistingTransaction(bobSession.myUserId, txId) - assertTrue(aliceKeyTx is SasVerificationTransaction) - - assertEquals("Alice and Bob have same transaction id", aliceKeyTx!!.transactionId, bobKeyTx!!.transactionId) - - val aliceCancelled = CompletableDeferred() - aliceVerificationService.requestEventFlow().onEach { - Log.d("#E2E", "alice flow event $it | ${it.getTransaction()?.dbgState()}") - val tx = it.getTransaction() - if (tx?.transactionId == txId && tx is SasVerificationTransaction) { - if (tx.state() is SasTransactionState.Cancelled) { - aliceCancelled.complete(tx.state() as SasTransactionState.Cancelled) - } - } - }.launchIn(scope) - - val bobCancelled = CompletableDeferred() - bobVerificationService.requestEventFlow().onEach { - Log.d("#E2E", "bob flow event $it | ${it.getTransaction()?.dbgState()}") - val tx = it.getTransaction() - if (tx?.transactionId == txId && tx is SasVerificationTransaction) { - if (tx.state() is SasTransactionState.Cancelled) { - bobCancelled.complete(tx.state() as SasTransactionState.Cancelled) - } - } - }.launchIn(scope) - - aliceVerificationService.cancelVerificationRequest(bobSession.myUserId, txId) - - val cancelledAlice = aliceCancelled.await() - val cancelledBob = bobCancelled.await() - - assertEquals("Should be User cancelled on alice side", CancelCode.User, cancelledAlice.cancelCode) - assertEquals("Should be User cancelled on bob side", CancelCode.User, cancelledBob.cancelCode) - - assertNull(bobVerificationService.getExistingTransaction(aliceSession.myUserId, txId)) - assertNull(aliceVerificationService.getExistingTransaction(bobSession.myUserId, txId)) - } - - /* -@Test -@Ignore("This test will be ignored until it is fixed") -fun test_key_agreement_protocols_must_include_curve25519() = runCryptoTest(context()) { cryptoTestHelper, testHelper -> - fail("Not passing for the moment") - val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom() - - val bobSession = cryptoTestData.secondSession!! - - val protocols = listOf("meh_dont_know") - val tid = "00000000" - - // Bob should receive a cancel - var cancelReason: CancelCode? = null - val cancelLatch = CountDownLatch(1) - - val bobListener = object : VerificationService.Listener { - override fun transactionUpdated(tx: VerificationTransaction) { - tx as SasVerificationTransaction - if (tx.transactionId == tid && tx.state() is SasTransactionState.Cancelled) { - cancelReason = (tx.state() as SasTransactionState.Cancelled).cancelCode - cancelLatch.countDown() - } - } - } -// bobSession.cryptoService().verificationService().addListener(bobListener) - - // TODO bobSession!!.dataHandler.addListener(object : MXEventListener() { - // TODO override fun onToDeviceEvent(event: Event?) { - // TODO if (event!!.getType() == CryptoEvent.EVENT_TYPE_KEY_VERIFICATION_CANCEL) { - // TODO if (event.contentAsJsonObject?.get("transaction_id")?.asString == tid) { - // TODO canceledToDeviceEvent = event - // TODO cancelLatch.countDown() - // TODO } - // TODO } - // TODO } - // TODO }) - - val aliceSession = cryptoTestData.firstSession - val aliceUserID = aliceSession.myUserId - val aliceDevice = aliceSession.cryptoService().getMyCryptoDevice().deviceId - - val aliceListener = object : VerificationService.Listener { - override fun transactionUpdated(tx: VerificationTransaction) { - tx as SasVerificationTransaction - if (tx.state() is SasTransactionState.SasStarted) { - runBlocking { - tx.acceptVerification() - } - } - } - } -// aliceSession.cryptoService().verificationService().addListener(aliceListener) - - fakeBobStart(bobSession, aliceUserID, aliceDevice, tid, protocols = protocols) - - testHelper.await(cancelLatch) - - assertEquals("Request should be cancelled with m.unknown_method", CancelCode.UnknownMethod, cancelReason) -} - -@Test -@Ignore("This test will be ignored until it is fixed") -fun test_key_agreement_macs_Must_include_hmac_sha256() = runCryptoTest(context()) { cryptoTestHelper, testHelper -> - fail("Not passing for the moment") - val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom() - - val bobSession = cryptoTestData.secondSession!! - - val mac = listOf("shaBit") - val tid = "00000000" - - // Bob should receive a cancel - val canceledToDeviceEvent: Event? = null - val cancelLatch = CountDownLatch(1) - // TODO bobSession!!.dataHandler.addListener(object : MXEventListener() { - // TODO override fun onToDeviceEvent(event: Event?) { - // TODO if (event!!.getType() == CryptoEvent.EVENT_TYPE_KEY_VERIFICATION_CANCEL) { - // TODO if (event.contentAsJsonObject?.get("transaction_id")?.asString == tid) { - // TODO canceledToDeviceEvent = event - // TODO cancelLatch.countDown() - // TODO } - // TODO } - // TODO } - // TODO }) - - val aliceSession = cryptoTestData.firstSession - val aliceUserID = aliceSession.myUserId - val aliceDevice = aliceSession.cryptoService().getMyCryptoDevice().deviceId - - fakeBobStart(bobSession, aliceUserID, aliceDevice, tid, mac = mac) - - testHelper.await(cancelLatch) - val cancelReq = canceledToDeviceEvent!!.content.toModel()!! - assertEquals("Request should be cancelled with m.unknown_method", CancelCode.UnknownMethod.value, cancelReq.code) -} - -@Test -@Ignore("This test will be ignored until it is fixed") -fun test_key_agreement_short_code_include_decimal() = runCryptoTest(context()) { cryptoTestHelper, testHelper -> - fail("Not passing for the moment") - val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom() - - val bobSession = cryptoTestData.secondSession!! - - val codes = listOf("bin", "foo", "bar") - val tid = "00000000" - - // Bob should receive a cancel - var canceledToDeviceEvent: Event? = null - val cancelLatch = CountDownLatch(1) - // TODO bobSession!!.dataHandler.addListener(object : MXEventListener() { - // TODO override fun onToDeviceEvent(event: Event?) { - // TODO if (event!!.getType() == CryptoEvent.EVENT_TYPE_KEY_VERIFICATION_CANCEL) { - // TODO if (event.contentAsJsonObject?.get("transaction_id")?.asString == tid) { - // TODO canceledToDeviceEvent = event - // TODO cancelLatch.countDown() - // TODO } - // TODO } - // TODO } - // TODO }) - - val aliceSession = cryptoTestData.firstSession - val aliceUserID = aliceSession.myUserId - val aliceDevice = aliceSession.cryptoService().getMyCryptoDevice().deviceId - - fakeBobStart(bobSession, aliceUserID, aliceDevice, tid, codes = codes) - - testHelper.await(cancelLatch) - - val cancelReq = canceledToDeviceEvent!!.content.toModel()!! - assertEquals("Request should be cancelled with m.unknown_method", CancelCode.UnknownMethod.value, cancelReq.code) -} - -private suspend fun fakeBobStart( - bobSession: Session, - aliceUserID: String?, - aliceDevice: String?, - tid: String, - protocols: List = SasVerificationTransaction.KNOWN_AGREEMENT_PROTOCOLS, - hashes: List = SasVerificationTransaction.KNOWN_HASHES, - mac: List = SasVerificationTransaction.KNOWN_MACS, - codes: List = SasVerificationTransaction.KNOWN_SHORT_CODES -) { - val startMessage = KeyVerificationStart( - fromDevice = bobSession.cryptoService().getMyCryptoDevice().deviceId, - method = VerificationMethod.SAS.toValue(), - transactionId = tid, - keyAgreementProtocols = protocols, - hashes = hashes, - messageAuthenticationCodes = mac, - shortAuthenticationStrings = codes - ) - - val contentMap = MXUsersDevicesMap() - contentMap.setObject(aliceUserID, aliceDevice, startMessage) - - // TODO val sendLatch = CountDownLatch(1) - // TODO bobSession.cryptoRestClient.sendToDevice( - // TODO EventType.KEY_VERIFICATION_START, - // TODO contentMap, - // TODO tid, - // TODO TestMatrixCallback(sendLatch) - // TODO ) -} - -// any two devices may only have at most one key verification in flight at a time. -// If a device has two verifications in progress with the same device, then it should cancel both verifications. -@Test -fun test_aliceStartTwoRequests() = runCryptoTest(context()) { cryptoTestHelper, testHelper -> - val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom() - - val aliceSession = cryptoTestData.firstSession - val bobSession = cryptoTestData.secondSession - - val aliceVerificationService = aliceSession.cryptoService().verificationService() - - val aliceCreatedLatch = CountDownLatch(2) - val aliceCancelledLatch = CountDownLatch(1) - val createdTx = mutableListOf() - val aliceListener = object : VerificationService.Listener { - override fun transactionCreated(tx: VerificationTransaction) { - createdTx.add(tx) - aliceCreatedLatch.countDown() - } - - override fun transactionUpdated(tx: VerificationTransaction) { - tx as SasVerificationTransaction - if (tx.state() is SasTransactionState.Cancelled && !(tx.state() as SasTransactionState.Cancelled).byMe) { - aliceCancelledLatch.countDown() - } - } - } -// aliceVerificationService.addListener(aliceListener) - - val bobUserId = bobSession!!.myUserId - val bobDeviceId = bobSession.cryptoService().getMyCryptoDevice().deviceId - - // TODO -// aliceSession.cryptoService().downloadKeysIfNeeded(listOf(bobUserId), forceDownload = true) -// aliceVerificationService.beginKeyVerification(listOf(VerificationMethod.SAS), bobUserId, bobDeviceId) -// aliceVerificationService.beginKeyVerification(bobUserId, bobDeviceId) -// testHelper.await(aliceCreatedLatch) -// testHelper.await(aliceCancelledLatch) - - cryptoTestData.cleanUp(testHelper) -} - -/** - * Test that when alice starts a 'correct' request, bob agrees. - */ -// @Test -// @Ignore("This test will be ignored until it is fixed") -// fun test_aliceAndBobAgreement() = runCryptoTest(context()) { cryptoTestHelper, testHelper -> -// val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom() -// -// val aliceSession = cryptoTestData.firstSession -// val bobSession = cryptoTestData.secondSession -// -// val aliceVerificationService = aliceSession.cryptoService().verificationService() -// val bobVerificationService = bobSession!!.cryptoService().verificationService() -// -// val aliceAcceptedLatch = CountDownLatch(1) -// val aliceListener = object : VerificationService.Listener { -// override fun transactionUpdated(tx: VerificationTransaction) { -// if (tx.state() is VerificationTxState.OnAccepted) { -// aliceAcceptedLatch.countDown() -// } -// } -// } -// aliceVerificationService.addListener(aliceListener) -// -// val bobListener = object : VerificationService.Listener { -// override fun transactionUpdated(tx: VerificationTransaction) { -// if (tx.state() is VerificationTxState.OnStarted && tx is SasVerificationTransaction) { -// bobVerificationService.removeListener(this) -// runBlocking { -// tx.acceptVerification() -// } -// } -// } -// } -// bobVerificationService.addListener(bobListener) -// -// val bobUserId = bobSession.myUserId -// val bobDeviceId = runBlocking { -// bobSession.cryptoService().getMyCryptoDevice().deviceId -// } -// -// aliceVerificationService.beginKeyVerification(VerificationMethod.SAS, bobUserId, bobDeviceId, null) -// testHelper.await(aliceAcceptedLatch) -// -// aliceVerificationService.getExistingTransaction(bobUserId, ) -// -// assertTrue("Should have receive a commitment", accepted!!.commitment?.trim()?.isEmpty() == false) -// -// // check that agreement is valid -// assertTrue("Agreed Protocol should be Valid", accepted != null) -// assertTrue("Agreed Protocol should be known by alice", startReq!!.keyAgreementProtocols.contains(accepted!!.keyAgreementProtocol)) -// assertTrue("Hash should be known by alice", startReq!!.hashes.contains(accepted!!.hash)) -// assertTrue("Hash should be known by alice", startReq!!.messageAuthenticationCodes.contains(accepted!!.messageAuthenticationCode)) -// -// accepted!!.shortAuthenticationStrings.forEach { -// assertTrue("all agreed Short Code should be known by alice", startReq!!.shortAuthenticationStrings.contains(it)) -// } -// } - -// @Test -// fun test_aliceAndBobSASCode() = runCryptoTest(context()) { cryptoTestHelper, testHelper -> -// val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom() -// cryptoTestData.initializeCrossSigning(cryptoTestHelper) -// val sasTestHelper = SasVerificationTestHelper(testHelper, cryptoTestHelper) -// val aliceSession = cryptoTestData.firstSession -// val bobSession = cryptoTestData.secondSession!! -// val transactionId = sasTestHelper.requestVerificationAndWaitForReadyState(cryptoTestData, supportedMethods) -// -// val latch = CountDownLatch(2) -// val aliceListener = object : VerificationService.Listener { -// override fun transactionUpdated(tx: VerificationTransaction) { -// Timber.v("Alice transactionUpdated: ${tx.state()}") -// latch.countDown() -// } -// } -// aliceSession.cryptoService().verificationService().addListener(aliceListener) -// val bobListener = object : VerificationService.Listener { -// override fun transactionUpdated(tx: VerificationTransaction) { -// Timber.v("Bob transactionUpdated: ${tx.state()}") -// latch.countDown() -// } -// } -// bobSession.cryptoService().verificationService().addListener(bobListener) -// aliceSession.cryptoService().verificationService().beginKeyVerification(VerificationMethod.SAS, bobSession.myUserId, transactionId) -// -// testHelper.await(latch) -// val aliceTx = -// aliceSession.cryptoService().verificationService().getExistingTransaction(bobSession.myUserId, transactionId) as SasVerificationTransaction -// val bobTx = bobSession.cryptoService().verificationService().getExistingTransaction(aliceSession.myUserId, transactionId) as SasVerificationTransaction -// -// assertEquals("Should have same SAS", aliceTx.getDecimalCodeRepresentation(), bobTx.getDecimalCodeRepresentation()) -// -// val aliceTx = aliceVerificationService.getExistingTransaction(bobUserId, verificationSAS!!) as SASDefaultVerificationTransaction -// val bobTx = bobVerificationService.getExistingTransaction(aliceSession.myUserId, verificationSAS) as SASDefaultVerificationTransaction -// -// assertEquals( -// "Should have same SAS", aliceTx.getShortCodeRepresentation(SasMode.DECIMAL), -// bobTx.getShortCodeRepresentation(SasMode.DECIMAL) -// ) -// } - -@Test -fun test_happyPath() = runCryptoTest(context()) { cryptoTestHelper, testHelper -> - val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom() - cryptoTestData.initializeCrossSigning(cryptoTestHelper) - val sasVerificationTestHelper = SasVerificationTestHelper(testHelper, cryptoTestHelper) - val transactionId = sasVerificationTestHelper.requestVerificationAndWaitForReadyState(cryptoTestData, listOf(VerificationMethod.SAS)) - val aliceSession = cryptoTestData.firstSession - val bobSession = cryptoTestData.secondSession - - val aliceVerificationService = aliceSession.cryptoService().verificationService() - val bobVerificationService = bobSession!!.cryptoService().verificationService() - - val verifiedLatch = CountDownLatch(2) - val aliceListener = object : VerificationService.Listener { - - override fun verificationRequestUpdated(pr: PendingVerificationRequest) { - Timber.v("RequestUpdated pr=$pr") - } - - var matched = false - var verified = false - override fun transactionUpdated(tx: VerificationTransaction) { - if (tx !is SasVerificationTransaction) return - Timber.v("Alice transactionUpdated: ${tx.state()} on thread:${Thread.currentThread()}") - when (tx.state()) { - SasTransactionState.SasShortCodeReady -> { - if (!matched) { - matched = true - runBlocking { - delay(500) - tx.userHasVerifiedShortCode() - } - } - } - is SasTransactionState.Done -> { - if (!verified) { - verified = true - verifiedLatch.countDown() - } - } - else -> Unit - } - } - } -// aliceVerificationService.addListener(aliceListener) - - val bobListener = object : VerificationService.Listener { - var accepted = false - var matched = false - var verified = false - - override fun verificationRequestUpdated(pr: PendingVerificationRequest) { - Timber.v("RequestUpdated: pr=$pr") - } - - override fun transactionUpdated(tx: VerificationTransaction) { - if (tx !is SasVerificationTransaction) return - Timber.v("Bob transactionUpdated: ${tx.state()} on thread: ${Thread.currentThread()}") - when (tx.state()) { -// VerificationTxState.SasStarted -> { -// if (!accepted) { -// accepted = true -// runBlocking { -// tx.acceptVerification() -// } -// } -// } - SasTransactionState.SasShortCodeReady -> { - if (!matched) { - matched = true - runBlocking { - delay(500) - tx.userHasVerifiedShortCode() - } - } - } - is SasTransactionState.Done -> { - if (!verified) { - verified = true - verifiedLatch.countDown() - } - } - else -> Unit - } - } - } -// bobVerificationService.addListener(bobListener) - - val bobUserId = bobSession.myUserId - val bobDeviceId = runBlocking { - bobSession.cryptoService().getMyCryptoDevice().deviceId - } - aliceVerificationService.startKeyVerification(VerificationMethod.SAS, bobUserId, transactionId) - - Timber.v("Await after beginKey ${Thread.currentThread()}") - testHelper.await(verifiedLatch) - - // Assert that devices are verified - val bobDeviceInfoFromAlicePOV: CryptoDeviceInfo? = aliceSession.cryptoService().getCryptoDeviceInfo(bobUserId, bobDeviceId) - val aliceDeviceInfoFromBobPOV: CryptoDeviceInfo? = - bobSession.cryptoService().getCryptoDeviceInfo(aliceSession.myUserId, aliceSession.cryptoService().getMyCryptoDevice().deviceId) - - assertTrue("alice device should be verified from bob point of view", aliceDeviceInfoFromBobPOV!!.isVerified) - assertTrue("bob device should be verified from alice point of view", bobDeviceInfoFromAlicePOV!!.isVerified) -} - -@Test -fun test_ConcurrentStart() = runCryptoTest(context()) { cryptoTestHelper, testHelper -> - val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom() - cryptoTestData.initializeCrossSigning(cryptoTestHelper) - val aliceSession = cryptoTestData.firstSession - val bobSession = cryptoTestData.secondSession!! - - val aliceVerificationService = aliceSession.cryptoService().verificationService() - val bobVerificationService = bobSession.cryptoService().verificationService() - - val req = aliceVerificationService.requestKeyVerificationInDMs( - listOf(VerificationMethod.SAS, VerificationMethod.QR_CODE_SCAN, VerificationMethod.QR_CODE_SHOW), - bobSession.myUserId, - cryptoTestData.roomId - ) - - val requestID = req.transactionId - - Log.v("TEST", "== requestID is $requestID") - - testHelper.retryPeriodically { - val prBobPOV = bobVerificationService.getExistingVerificationRequests(aliceSession.myUserId).firstOrNull() - Log.v("TEST", "== prBobPOV is $prBobPOV") - prBobPOV?.transactionId == requestID - } - - bobVerificationService.readyPendingVerification( - listOf(VerificationMethod.SAS, VerificationMethod.QR_CODE_SCAN, VerificationMethod.QR_CODE_SHOW), - aliceSession.myUserId, - requestID - ) - - // wait for alice to get the ready - testHelper.retryPeriodically { - val prAlicePOV = aliceVerificationService.getExistingVerificationRequests(bobSession.myUserId).firstOrNull() - Log.v("TEST", "== prAlicePOV is $prAlicePOV") - prAlicePOV?.transactionId == requestID && prAlicePOV.state == EVerificationState.Ready - } - - // Start concurrent! - aliceVerificationService.startKeyVerification( - method = VerificationMethod.SAS, - otherUserId = bobSession.myUserId, - requestId = requestID, - ) - - bobVerificationService.startKeyVerification( - method = VerificationMethod.SAS, - otherUserId = aliceSession.myUserId, - requestId = requestID, - ) - - // we should reach SHOW SAS on both - var alicePovTx: SasVerificationTransaction? - var bobPovTx: SasVerificationTransaction? - - testHelper.retryPeriodically { - alicePovTx = aliceVerificationService.getExistingTransaction(bobSession.myUserId, requestID) as? SasVerificationTransaction - Log.v("TEST", "== alicePovTx is $alicePovTx") - alicePovTx?.state() == SasTransactionState.SasShortCodeReady - } - // wait for alice to get the ready - testHelper.retryPeriodically { - bobPovTx = bobVerificationService.getExistingTransaction(aliceSession.myUserId, requestID) as? SasVerificationTransaction - Log.v("TEST", "== bobPovTx is $bobPovTx") - bobPovTx?.state() == SasTransactionState.SasShortCodeReady - } -} - - */ -} diff --git a/matrix-sdk-android/src/androidTestKotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/QrCodeTest.kt b/matrix-sdk-android/src/androidTestKotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/QrCodeTest.kt deleted file mode 100644 index d7b4d636fc..0000000000 --- a/matrix-sdk-android/src/androidTestKotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/QrCodeTest.kt +++ /dev/null @@ -1,251 +0,0 @@ -/* - * Copyright 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. - */ - -package org.matrix.android.sdk.internal.crypto.verification.qrcode - -import androidx.test.ext.junit.runners.AndroidJUnit4 -import org.amshove.kluent.shouldBeEqualTo -import org.amshove.kluent.shouldBeNull -import org.amshove.kluent.shouldNotBeNull -import org.junit.FixMethodOrder -import org.junit.Test -import org.junit.runner.RunWith -import org.junit.runners.MethodSorters -import org.matrix.android.sdk.InstrumentedTest - -@RunWith(AndroidJUnit4::class) -@FixMethodOrder(MethodSorters.JVM) -class QrCodeTest : InstrumentedTest { - - private val qrCode1 = QrCodeData.VerifyingAnotherUser( - transactionId = "MaTransaction", - userMasterCrossSigningPublicKey = "ktEwcUP6su1xh+GuE+CYkQ3H6W/DIl+ybHFdaEOrolU", - otherUserMasterCrossSigningPublicKey = "TXluZKTZLvSRWOTPlOqLq534bA+/K4zLFKSu9cGLQaU", - sharedSecret = "MTIzNDU2Nzg" - ) - - private val value1 = - "MATRIX\u0002\u0000\u0000\u000DMaTransaction\u0092Ñ0qCú²íq\u0087á®\u0013à\u0098\u0091\u000DÇéoÃ\"_²lq]hC«¢UMynd¤Ù.ô\u0091XäÏ\u0094ê\u008B«\u009Døl\u000F¿+\u008CË\u0014¤®õÁ\u008BA¥12345678" - - private val qrCode2 = QrCodeData.SelfVerifyingMasterKeyTrusted( - transactionId = "MaTransaction", - userMasterCrossSigningPublicKey = "ktEwcUP6su1xh+GuE+CYkQ3H6W/DIl+ybHFdaEOrolU", - otherDeviceKey = "TXluZKTZLvSRWOTPlOqLq534bA+/K4zLFKSu9cGLQaU", - sharedSecret = "MTIzNDU2Nzg" - ) - - private val value2 = - "MATRIX\u0002\u0001\u0000\u000DMaTransaction\u0092Ñ0qCú²íq\u0087á®\u0013à\u0098\u0091\u000DÇéoÃ\"_²lq]hC«¢UMynd¤Ù.ô\u0091XäÏ\u0094ê\u008B«\u009Døl\u000F¿+\u008CË\u0014¤®õÁ\u008BA¥12345678" - - private val qrCode3 = QrCodeData.SelfVerifyingMasterKeyNotTrusted( - transactionId = "MaTransaction", - deviceKey = "TXluZKTZLvSRWOTPlOqLq534bA+/K4zLFKSu9cGLQaU", - userMasterCrossSigningPublicKey = "ktEwcUP6su1xh+GuE+CYkQ3H6W/DIl+ybHFdaEOrolU", - sharedSecret = "MTIzNDU2Nzg" - ) - - private val value3 = - "MATRIX\u0002\u0002\u0000\u000DMaTransactionMynd¤Ù.ô\u0091XäÏ\u0094ê\u008B«\u009Døl\u000F¿+\u008CË\u0014¤®õÁ\u008BA¥\u0092Ñ0qCú²íq\u0087á®\u0013à\u0098\u0091\u000DÇéoÃ\"_²lq]hC«¢U12345678" - - private val sharedSecretByteArray = "12345678".toByteArray(Charsets.ISO_8859_1) - - private val tlx_byteArray = hexToByteArray("4d 79 6e 64 a4 d9 2e f4 91 58 e4 cf 94 ea 8b ab 9d f8 6c 0f bf 2b 8c cb 14 a4 ae f5 c1 8b 41 a5") - - private val kte_byteArray = hexToByteArray("92 d1 30 71 43 fa b2 ed 71 87 e1 ae 13 e0 98 91 0d c7 e9 6f c3 22 5f b2 6c 71 5d 68 43 ab a2 55") - - @Test - fun testEncoding1() { - qrCode1.toEncodedString() shouldBeEqualTo value1 - } - - @Test - fun testEncoding2() { - qrCode2.toEncodedString() shouldBeEqualTo value2 - } - - @Test - fun testEncoding3() { - qrCode3.toEncodedString() shouldBeEqualTo value3 - } - - @Test - fun testSymmetry1() { - qrCode1.toEncodedString().toQrCodeData() shouldBeEqualTo qrCode1 - } - - @Test - fun testSymmetry2() { - qrCode2.toEncodedString().toQrCodeData() shouldBeEqualTo qrCode2 - } - - @Test - fun testSymmetry3() { - qrCode3.toEncodedString().toQrCodeData() shouldBeEqualTo qrCode3 - } - - @Test - fun testCase1() { - val url = qrCode1.toEncodedString() - - val byteArray = url.toByteArray(Charsets.ISO_8859_1) - checkHeader(byteArray) - - // Mode - byteArray[7] shouldBeEqualTo 0 - - checkSizeAndTransaction(byteArray) - - compareArray(byteArray.copyOfRange(23, 23 + 32), kte_byteArray) - compareArray(byteArray.copyOfRange(23 + 32, 23 + 64), tlx_byteArray) - - compareArray(byteArray.copyOfRange(23 + 64, byteArray.size), sharedSecretByteArray) - } - - @Test - fun testCase2() { - val url = qrCode2.toEncodedString() - - val byteArray = url.toByteArray(Charsets.ISO_8859_1) - checkHeader(byteArray) - - // Mode - byteArray[7] shouldBeEqualTo 1 - - checkSizeAndTransaction(byteArray) - compareArray(byteArray.copyOfRange(23, 23 + 32), kte_byteArray) - compareArray(byteArray.copyOfRange(23 + 32, 23 + 64), tlx_byteArray) - - compareArray(byteArray.copyOfRange(23 + 64, byteArray.size), sharedSecretByteArray) - } - - @Test - fun testCase3() { - val url = qrCode3.toEncodedString() - - val byteArray = url.toByteArray(Charsets.ISO_8859_1) - checkHeader(byteArray) - - // Mode - byteArray[7] shouldBeEqualTo 2 - - checkSizeAndTransaction(byteArray) - compareArray(byteArray.copyOfRange(23, 23 + 32), tlx_byteArray) - compareArray(byteArray.copyOfRange(23 + 32, 23 + 64), kte_byteArray) - - compareArray(byteArray.copyOfRange(23 + 64, byteArray.size), sharedSecretByteArray) - } - - @Test - fun testLongTransactionId() { - // Size on two bytes (2_000 = 0x07D0) - val longTransactionId = "PatternId_".repeat(200) - - val qrCode = qrCode1.copy(transactionId = longTransactionId) - - val result = qrCode.toEncodedString() - val expected = value1.replace("\u0000\u000DMaTransaction", "\u0007\u00D0$longTransactionId") - - result shouldBeEqualTo expected - - // Reverse operation - expected.toQrCodeData() shouldBeEqualTo qrCode - } - - @Test - fun testAnyTransactionId() { - for (qty in 0 until 0x1FFF step 200) { - val longTransactionId = "a".repeat(qty) - - val qrCode = qrCode1.copy(transactionId = longTransactionId) - - // Symmetric operation - qrCode.toEncodedString().toQrCodeData() shouldBeEqualTo qrCode - } - } - - // Error cases - @Test - fun testErrorHeader() { - value1.replace("MATRIX", "MOTRIX").toQrCodeData().shouldBeNull() - value1.replace("MATRIX", "MATRI").toQrCodeData().shouldBeNull() - value1.replace("MATRIX", "").toQrCodeData().shouldBeNull() - } - - @Test - fun testErrorVersion() { - value1.replace("MATRIX\u0002", "MATRIX\u0000").toQrCodeData().shouldBeNull() - value1.replace("MATRIX\u0002", "MATRIX\u0001").toQrCodeData().shouldBeNull() - value1.replace("MATRIX\u0002", "MATRIX\u0003").toQrCodeData().shouldBeNull() - value1.replace("MATRIX\u0002", "MATRIX").toQrCodeData().shouldBeNull() - } - - @Test - fun testErrorSecretTooShort() { - value1.replace("12345678", "1234567").toQrCodeData().shouldBeNull() - } - - @Test - fun testErrorNoTransactionNoKeyNoSecret() { - // But keep transaction length - "MATRIX\u0002\u0000\u0000\u000D".toQrCodeData().shouldBeNull() - } - - @Test - fun testErrorNoKeyNoSecret() { - "MATRIX\u0002\u0000\u0000\u000DMaTransaction".toQrCodeData().shouldBeNull() - } - - @Test - fun testErrorTransactionLengthTooShort() { - // In this case, the secret will be longer, so this is not an error, but it will lead to keys mismatch - value1.replace("\u000DMaTransaction", "\u000CMaTransaction").toQrCodeData().shouldNotBeNull() - } - - @Test - fun testErrorTransactionLengthTooBig() { - value1.replace("\u000DMaTransaction", "\u000EMaTransaction").toQrCodeData().shouldBeNull() - } - - private fun compareArray(actual: ByteArray, expected: ByteArray) { - actual.size shouldBeEqualTo expected.size - - for (i in actual.indices) { - actual[i] shouldBeEqualTo expected[i] - } - } - - private fun checkHeader(byteArray: ByteArray) { - // MATRIX - byteArray[0] shouldBeEqualTo 'M'.code.toByte() - byteArray[1] shouldBeEqualTo 'A'.code.toByte() - byteArray[2] shouldBeEqualTo 'T'.code.toByte() - byteArray[3] shouldBeEqualTo 'R'.code.toByte() - byteArray[4] shouldBeEqualTo 'I'.code.toByte() - byteArray[5] shouldBeEqualTo 'X'.code.toByte() - - // Version - byteArray[6] shouldBeEqualTo 2 - } - - private fun checkSizeAndTransaction(byteArray: ByteArray) { - // Size - byteArray[8] shouldBeEqualTo 0 - byteArray[9] shouldBeEqualTo 13 - - // Transaction - byteArray.copyOfRange(10, 10 + "MaTransaction".length).toString(Charsets.ISO_8859_1) shouldBeEqualTo "MaTransaction" - } -} diff --git a/matrix-sdk-android/src/androidTestKotlinCrypto/java/org/matrix/android/sdk/internal/database/CryptoSanityMigrationTest.kt b/matrix-sdk-android/src/androidTestKotlinCrypto/java/org/matrix/android/sdk/internal/database/CryptoSanityMigrationTest.kt deleted file mode 100644 index b4f07eff5a..0000000000 --- a/matrix-sdk-android/src/androidTestKotlinCrypto/java/org/matrix/android/sdk/internal/database/CryptoSanityMigrationTest.kt +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Copyright 2022 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. - */ - -package org.matrix.android.sdk.internal.database - -import android.content.Context -import androidx.test.platform.app.InstrumentationRegistry -import io.realm.Realm -import org.junit.After -import org.junit.Before -import org.junit.Rule -import org.junit.Test -import org.matrix.android.sdk.internal.crypto.store.db.RealmCryptoStoreMigration -import org.matrix.android.sdk.internal.crypto.store.db.RealmCryptoStoreModule -import org.matrix.android.sdk.internal.util.time.Clock - -class CryptoSanityMigrationTest { - @get:Rule val configurationFactory = TestRealmConfigurationFactory() - - lateinit var context: Context - var realm: Realm? = null - - @Before - fun setUp() { - context = InstrumentationRegistry.getInstrumentation().context - } - - @After - fun tearDown() { - realm?.close() - } - - @Test - fun cryptoDatabaseShouldMigrateGracefully() { - val realmName = "crypto_store_20.realm" - - val migration = RealmCryptoStoreMigration( - object : Clock { - override fun epochMillis(): Long { - return 0L - } - } - ) - - val realmConfiguration = configurationFactory.createConfiguration( - realmName, - "7b9a21a8a311e85d75b069a343c23fc952fc3fec5e0c83ecfa13f24b787479c487c3ed587db3dd1f5805d52041fc0ac246516e94b27ffa699ff928622e621aca", - RealmCryptoStoreModule(), - migration.schemaVersion, - migration - ) - configurationFactory.copyRealmFromAssets(context, realmName, realmName) - - realm = Realm.getInstance(realmConfiguration) - } -} diff --git a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/api/session/crypto/keysbackup/BackupRecoveryKey.kt b/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/api/session/crypto/keysbackup/BackupRecoveryKey.kt deleted file mode 100644 index 39c4bfd5f8..0000000000 --- a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/api/session/crypto/keysbackup/BackupRecoveryKey.kt +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright 2022 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. - */ - -package org.matrix.android.sdk.api.session.crypto.keysbackup - -import org.matrix.android.sdk.api.util.toBase64NoPadding -import org.matrix.android.sdk.internal.crypto.tools.withOlmDecryption -import org.matrix.olm.OlmPkMessage - -class BackupRecoveryKey(private val key: ByteArray) : IBackupRecoveryKey { - - override fun equals(other: Any?): Boolean { - if (other !is BackupRecoveryKey) return false - return this.toBase58() == other.toBase58() - } - - override fun hashCode(): Int { - return key.contentHashCode() - } - - override fun toBase58() = computeRecoveryKey(key) - - override fun toBase64() = key.toBase64NoPadding() - - override fun decryptV1(ephemeralKey: String, mac: String, ciphertext: String): String = withOlmDecryption { - it.setPrivateKey(key) - it.decrypt(OlmPkMessage().apply { - this.mEphemeralKey = ephemeralKey - this.mCipherText = ciphertext - this.mMac = mac - }) - } - - override fun megolmV1PublicKey() = v1pk - - private val v1pk = object : IMegolmV1PublicKey { - override val publicKey: String - get() = withOlmDecryption { - it.setPrivateKey(key) - } - override val privateKeySalt: String? - get() = null // not use in kotlin sdk - override val privateKeyIterations: Int? - get() = null // not use in kotlin sdk - override val backupAlgorithm: String - get() = "" // not use in kotlin sdk - } -} diff --git a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/api/session/crypto/keysbackup/BackupUtils.kt b/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/api/session/crypto/keysbackup/BackupUtils.kt deleted file mode 100644 index e44186a09b..0000000000 --- a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/api/session/crypto/keysbackup/BackupUtils.kt +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright 2022 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. - */ - -package org.matrix.android.sdk.api.session.crypto.keysbackup - -import org.matrix.android.sdk.internal.crypto.keysbackup.generatePrivateKeyWithPassword - -object BackupUtils { - - fun recoveryKeyFromBase58(base58: String): IBackupRecoveryKey? { - return extractCurveKeyFromRecoveryKey(base58)?.let { - BackupRecoveryKey(it) - } - } - - fun recoveryKeyFromPassphrase(passphrase: String): IBackupRecoveryKey? { - return BackupRecoveryKey(generatePrivateKeyWithPassword(passphrase, null).privateKey) - } -} diff --git a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/api/session/room/model/message/MessageVerificationAcceptContent.kt b/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/api/session/room/model/message/MessageVerificationAcceptContent.kt deleted file mode 100644 index 33f61648dc..0000000000 --- a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/api/session/room/model/message/MessageVerificationAcceptContent.kt +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Copyright 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. - */ -package org.matrix.android.sdk.api.session.room.model.message - -import com.squareup.moshi.Json -import com.squareup.moshi.JsonClass -import org.matrix.android.sdk.api.session.events.model.RelationType -import org.matrix.android.sdk.api.session.events.model.toContent -import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent -import org.matrix.android.sdk.internal.crypto.verification.VerificationInfoAccept -import org.matrix.android.sdk.internal.crypto.verification.VerificationInfoAcceptFactory - -@JsonClass(generateAdapter = true) -internal data class MessageVerificationAcceptContent( - @Json(name = "hash") override val hash: String?, - @Json(name = "key_agreement_protocol") override val keyAgreementProtocol: String?, - @Json(name = "message_authentication_code") override val messageAuthenticationCode: String?, - @Json(name = "short_authentication_string") override val shortAuthenticationStrings: List?, - @Json(name = "m.relates_to") val relatesTo: RelationDefaultContent?, - @Json(name = "commitment") override var commitment: String? = null -) : VerificationInfoAccept { - - override val transactionId: String? - get() = relatesTo?.eventId - - override fun toEventContent() = toContent() - - companion object : VerificationInfoAcceptFactory { - - override fun create( - tid: String, - keyAgreementProtocol: String, - hash: String, - commitment: String, - messageAuthenticationCode: String, - shortAuthenticationStrings: List - ): VerificationInfoAccept { - return MessageVerificationAcceptContent( - hash, - keyAgreementProtocol, - messageAuthenticationCode, - shortAuthenticationStrings, - RelationDefaultContent( - RelationType.REFERENCE, - tid - ), - commitment - ) - } - } -} diff --git a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/api/session/room/model/message/MessageVerificationCancelContent.kt b/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/api/session/room/model/message/MessageVerificationCancelContent.kt deleted file mode 100644 index 687e5362d8..0000000000 --- a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/api/session/room/model/message/MessageVerificationCancelContent.kt +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright 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. - */ -package org.matrix.android.sdk.api.session.room.model.message - -import com.squareup.moshi.Json -import com.squareup.moshi.JsonClass -import org.matrix.android.sdk.api.session.crypto.verification.CancelCode -import org.matrix.android.sdk.api.session.events.model.RelationType -import org.matrix.android.sdk.api.session.events.model.toContent -import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent -import org.matrix.android.sdk.internal.crypto.verification.VerificationInfoCancel - -@JsonClass(generateAdapter = true) -data class MessageVerificationCancelContent( - @Json(name = "code") override val code: String? = null, - @Json(name = "reason") override val reason: String? = null, - @Json(name = "m.relates_to") val relatesTo: RelationDefaultContent? -) : VerificationInfoCancel { - - override val transactionId: String? - get() = relatesTo?.eventId - - override fun toEventContent() = toContent() - - companion object { - fun create(transactionId: String, reason: CancelCode): MessageVerificationCancelContent { - return MessageVerificationCancelContent( - reason.value, - reason.humanReadable, - RelationDefaultContent( - RelationType.REFERENCE, - transactionId - ) - ) - } - } -} diff --git a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/api/session/room/model/message/MessageVerificationDoneContent.kt b/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/api/session/room/model/message/MessageVerificationDoneContent.kt deleted file mode 100644 index 40301bdf5b..0000000000 --- a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/api/session/room/model/message/MessageVerificationDoneContent.kt +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright 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. - */ -package org.matrix.android.sdk.api.session.room.model.message - -import com.squareup.moshi.Json -import com.squareup.moshi.JsonClass -import org.matrix.android.sdk.api.session.events.model.Content -import org.matrix.android.sdk.api.session.events.model.toContent -import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent -import org.matrix.android.sdk.internal.crypto.verification.VerificationInfo - -@JsonClass(generateAdapter = true) -internal data class MessageVerificationDoneContent( - @Json(name = "m.relates_to") val relatesTo: RelationDefaultContent? -) : VerificationInfo { - - override val transactionId: String? - get() = relatesTo?.eventId - - override fun toEventContent(): Content? = toContent() - - override fun asValidObject(): ValidVerificationDone? { - val validTransactionId = transactionId?.takeIf { it.isNotEmpty() } ?: return null - - return ValidVerificationDone( - validTransactionId - ) - } -} - -internal data class ValidVerificationDone( - val transactionId: String -) diff --git a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/api/session/room/model/message/MessageVerificationKeyContent.kt b/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/api/session/room/model/message/MessageVerificationKeyContent.kt deleted file mode 100644 index 25aaac14b8..0000000000 --- a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/api/session/room/model/message/MessageVerificationKeyContent.kt +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright 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. - */ -package org.matrix.android.sdk.api.session.room.model.message - -import com.squareup.moshi.Json -import com.squareup.moshi.JsonClass -import org.matrix.android.sdk.api.session.events.model.RelationType -import org.matrix.android.sdk.api.session.events.model.toContent -import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent -import org.matrix.android.sdk.internal.crypto.verification.VerificationInfoKey -import org.matrix.android.sdk.internal.crypto.verification.VerificationInfoKeyFactory - -@JsonClass(generateAdapter = true) -internal data class MessageVerificationKeyContent( - /** - * The device’s ephemeral public key, as an unpadded base64 string. - */ - @Json(name = "key") override val key: String? = null, - @Json(name = "m.relates_to") val relatesTo: RelationDefaultContent? -) : VerificationInfoKey { - - override val transactionId: String? - get() = relatesTo?.eventId - - override fun toEventContent() = toContent() - - companion object : VerificationInfoKeyFactory { - - override fun create(tid: String, pubKey: String): VerificationInfoKey { - return MessageVerificationKeyContent( - pubKey, - RelationDefaultContent( - RelationType.REFERENCE, - tid - ) - ) - } - } -} diff --git a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/api/session/room/model/message/MessageVerificationMacContent.kt b/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/api/session/room/model/message/MessageVerificationMacContent.kt deleted file mode 100644 index 3bb3338491..0000000000 --- a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/api/session/room/model/message/MessageVerificationMacContent.kt +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright 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. - */ -package org.matrix.android.sdk.api.session.room.model.message - -import com.squareup.moshi.Json -import com.squareup.moshi.JsonClass -import org.matrix.android.sdk.api.session.events.model.RelationType -import org.matrix.android.sdk.api.session.events.model.toContent -import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent -import org.matrix.android.sdk.internal.crypto.verification.VerificationInfoMac -import org.matrix.android.sdk.internal.crypto.verification.VerificationInfoMacFactory - -@JsonClass(generateAdapter = true) -internal data class MessageVerificationMacContent( - @Json(name = "mac") override val mac: Map? = null, - @Json(name = "keys") override val keys: String? = null, - @Json(name = "m.relates_to") val relatesTo: RelationDefaultContent? -) : VerificationInfoMac { - - override val transactionId: String? - get() = relatesTo?.eventId - - override fun toEventContent() = toContent() - - companion object : VerificationInfoMacFactory { - override fun create(tid: String, mac: Map, keys: String): VerificationInfoMac { - return MessageVerificationMacContent( - mac, - keys, - RelationDefaultContent( - RelationType.REFERENCE, - tid - ) - ) - } - } -} diff --git a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/api/session/room/model/message/MessageVerificationReadyContent.kt b/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/api/session/room/model/message/MessageVerificationReadyContent.kt deleted file mode 100644 index 72bf6e6ff7..0000000000 --- a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/api/session/room/model/message/MessageVerificationReadyContent.kt +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright 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. - */ -package org.matrix.android.sdk.api.session.room.model.message - -import com.squareup.moshi.Json -import com.squareup.moshi.JsonClass -import org.matrix.android.sdk.api.session.events.model.RelationType -import org.matrix.android.sdk.api.session.events.model.toContent -import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent -import org.matrix.android.sdk.internal.crypto.verification.MessageVerificationReadyFactory -import org.matrix.android.sdk.internal.crypto.verification.VerificationInfoReady - -@JsonClass(generateAdapter = true) -internal data class MessageVerificationReadyContent( - @Json(name = "from_device") override val fromDevice: String? = null, - @Json(name = "methods") override val methods: List? = null, - @Json(name = "m.relates_to") val relatesTo: RelationDefaultContent? -) : VerificationInfoReady { - - override val transactionId: String? - get() = relatesTo?.eventId - - override fun toEventContent() = toContent() - - companion object : MessageVerificationReadyFactory { - override fun create(tid: String, methods: List, fromDevice: String): VerificationInfoReady { - return MessageVerificationReadyContent( - fromDevice = fromDevice, - methods = methods, - relatesTo = RelationDefaultContent( - RelationType.REFERENCE, - tid - ) - ) - } - } -} diff --git a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/api/session/room/model/message/MessageVerificationRequestContent.kt b/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/api/session/room/model/message/MessageVerificationRequestContent.kt deleted file mode 100644 index 5ea2fef7c2..0000000000 --- a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/api/session/room/model/message/MessageVerificationRequestContent.kt +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright 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. - */ -package org.matrix.android.sdk.api.session.room.model.message - -import com.squareup.moshi.Json -import com.squareup.moshi.JsonClass -import org.matrix.android.sdk.api.session.events.model.Content -import org.matrix.android.sdk.api.session.events.model.toContent -import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent -import org.matrix.android.sdk.internal.crypto.verification.VerificationInfoRequest - -@JsonClass(generateAdapter = true) -data class MessageVerificationRequestContent( - @Json(name = MessageContent.MSG_TYPE_JSON_KEY) override val msgType: String = MessageType.MSGTYPE_VERIFICATION_REQUEST, - @Json(name = "body") override val body: String, - @Json(name = "from_device") override val fromDevice: String?, - @Json(name = "methods") override val methods: List, - @Json(name = "to") val toUserId: String, - @Json(name = "timestamp") override val timestamp: Long?, - @Json(name = "format") val format: String? = null, - @Json(name = "formatted_body") val formattedBody: String? = null, - @Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null, - @Json(name = "m.new_content") override val newContent: Content? = null, - // Not parsed, but set after, using the eventId - override val transactionId: String? = null -) : MessageContent, VerificationInfoRequest { - - override fun toEventContent() = toContent() -} diff --git a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/api/session/room/model/message/MessageVerificationStartContent.kt b/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/api/session/room/model/message/MessageVerificationStartContent.kt deleted file mode 100644 index 8f23a9e150..0000000000 --- a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/api/session/room/model/message/MessageVerificationStartContent.kt +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright 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. - */ -package org.matrix.android.sdk.api.session.room.model.message - -import com.squareup.moshi.Json -import com.squareup.moshi.JsonClass -import org.matrix.android.sdk.api.session.events.model.toContent -import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent -import org.matrix.android.sdk.internal.crypto.verification.VerificationInfoStart -import org.matrix.android.sdk.internal.util.JsonCanonicalizer - -@JsonClass(generateAdapter = true) -internal data class MessageVerificationStartContent( - @Json(name = "from_device") override val fromDevice: String?, - @Json(name = "hashes") override val hashes: List?, - @Json(name = "key_agreement_protocols") override val keyAgreementProtocols: List?, - @Json(name = "message_authentication_codes") override val messageAuthenticationCodes: List?, - @Json(name = "short_authentication_string") override val shortAuthenticationStrings: List?, - @Json(name = "method") override val method: String?, - @Json(name = "m.relates_to") val relatesTo: RelationDefaultContent?, - @Json(name = "secret") override val sharedSecret: String? -) : VerificationInfoStart { - - override fun toCanonicalJson(): String { - return JsonCanonicalizer.getCanonicalJson(MessageVerificationStartContent::class.java, this) - } - - override val transactionId: String? - get() = relatesTo?.eventId - - override fun toEventContent() = toContent() -} diff --git a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/CryptoModule.kt b/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/CryptoModule.kt deleted file mode 100644 index 719c45a113..0000000000 --- a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/CryptoModule.kt +++ /dev/null @@ -1,266 +0,0 @@ -/* - * Copyright (c) 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. - */ - -package org.matrix.android.sdk.internal.crypto - -import dagger.Binds -import dagger.Module -import dagger.Provides -import io.realm.RealmConfiguration -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.SupervisorJob -import org.matrix.android.sdk.api.MatrixCoroutineDispatchers -import org.matrix.android.sdk.api.session.crypto.CryptoService -import org.matrix.android.sdk.api.session.crypto.crosssigning.CrossSigningService -import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupService -import org.matrix.android.sdk.api.session.crypto.verification.VerificationService -import org.matrix.android.sdk.internal.crypto.api.CryptoApi -import org.matrix.android.sdk.internal.crypto.crosssigning.DefaultCrossSigningService -import org.matrix.android.sdk.internal.crypto.keysbackup.DefaultKeysBackupService -import org.matrix.android.sdk.internal.crypto.keysbackup.api.RoomKeysApi -import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.CreateKeysBackupVersionTask -import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.DefaultCreateKeysBackupVersionTask -import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.DefaultDeleteBackupTask -import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.DefaultDeleteRoomSessionDataTask -import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.DefaultDeleteRoomSessionsDataTask -import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.DefaultDeleteSessionsDataTask -import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.DefaultGetKeysBackupLastVersionTask -import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.DefaultGetKeysBackupVersionTask -import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.DefaultGetRoomSessionDataTask -import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.DefaultGetRoomSessionsDataTask -import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.DefaultGetSessionsDataTask -import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.DefaultStoreRoomSessionDataTask -import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.DefaultStoreRoomSessionsDataTask -import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.DefaultStoreSessionsDataTask -import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.DefaultUpdateKeysBackupVersionTask -import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.DeleteBackupTask -import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.DeleteRoomSessionDataTask -import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.DeleteRoomSessionsDataTask -import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.DeleteSessionsDataTask -import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.GetKeysBackupLastVersionTask -import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.GetKeysBackupVersionTask -import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.GetRoomSessionDataTask -import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.GetRoomSessionsDataTask -import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.GetSessionsDataTask -import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.StoreRoomSessionDataTask -import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.StoreRoomSessionsDataTask -import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.StoreSessionsDataTask -import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.UpdateKeysBackupVersionTask -import org.matrix.android.sdk.internal.crypto.store.IMXCommonCryptoStore -import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore -import org.matrix.android.sdk.internal.crypto.store.db.RealmCryptoStore -import org.matrix.android.sdk.internal.crypto.store.db.RealmCryptoStoreMigration -import org.matrix.android.sdk.internal.crypto.store.db.RealmCryptoStoreModule -import org.matrix.android.sdk.internal.crypto.tasks.ClaimOneTimeKeysForUsersDeviceTask -import org.matrix.android.sdk.internal.crypto.tasks.DefaultClaimOneTimeKeysForUsersDevice -import org.matrix.android.sdk.internal.crypto.tasks.DefaultDeleteDeviceTask -import org.matrix.android.sdk.internal.crypto.tasks.DefaultDownloadKeysForUsers -import org.matrix.android.sdk.internal.crypto.tasks.DefaultEncryptEventTask -import org.matrix.android.sdk.internal.crypto.tasks.DefaultGetDeviceInfoTask -import org.matrix.android.sdk.internal.crypto.tasks.DefaultGetDevicesTask -import org.matrix.android.sdk.internal.crypto.tasks.DefaultInitializeCrossSigningTask -import org.matrix.android.sdk.internal.crypto.tasks.DefaultSendEventTask -import org.matrix.android.sdk.internal.crypto.tasks.DefaultSendToDeviceTask -import org.matrix.android.sdk.internal.crypto.tasks.DefaultSendVerificationMessageTask -import org.matrix.android.sdk.internal.crypto.tasks.DefaultSetDeviceNameTask -import org.matrix.android.sdk.internal.crypto.tasks.DefaultUploadKeysTask -import org.matrix.android.sdk.internal.crypto.tasks.DefaultUploadSignaturesTask -import org.matrix.android.sdk.internal.crypto.tasks.DefaultUploadSigningKeysTask -import org.matrix.android.sdk.internal.crypto.tasks.DeleteDeviceTask -import org.matrix.android.sdk.internal.crypto.tasks.DownloadKeysForUsersTask -import org.matrix.android.sdk.internal.crypto.tasks.EncryptEventTask -import org.matrix.android.sdk.internal.crypto.tasks.GetDeviceInfoTask -import org.matrix.android.sdk.internal.crypto.tasks.GetDevicesTask -import org.matrix.android.sdk.internal.crypto.tasks.InitializeCrossSigningTask -import org.matrix.android.sdk.internal.crypto.tasks.SendEventTask -import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask -import org.matrix.android.sdk.internal.crypto.tasks.SendVerificationMessageTask -import org.matrix.android.sdk.internal.crypto.tasks.SetDeviceNameTask -import org.matrix.android.sdk.internal.crypto.tasks.UploadKeysTask -import org.matrix.android.sdk.internal.crypto.tasks.UploadSignaturesTask -import org.matrix.android.sdk.internal.crypto.tasks.UploadSigningKeysTask -import org.matrix.android.sdk.internal.crypto.verification.DefaultVerificationService -import org.matrix.android.sdk.internal.database.RealmKeysUtils -import org.matrix.android.sdk.internal.di.CryptoDatabase -import org.matrix.android.sdk.internal.di.SessionFilesDirectory -import org.matrix.android.sdk.internal.di.UserMd5 -import org.matrix.android.sdk.internal.session.SessionScope -import org.matrix.android.sdk.internal.session.cache.ClearCacheTask -import org.matrix.android.sdk.internal.session.cache.RealmClearCacheTask -import retrofit2.Retrofit -import java.io.File - -@Module -internal abstract class CryptoModule { - - @Module - companion object { - internal fun getKeyAlias(userMd5: String) = "crypto_module_$userMd5" - - @JvmStatic - @Provides - @CryptoDatabase - @SessionScope - fun providesRealmConfiguration( - @SessionFilesDirectory directory: File, - @UserMd5 userMd5: String, - realmKeysUtils: RealmKeysUtils, - realmCryptoStoreMigration: RealmCryptoStoreMigration - ): RealmConfiguration { - return RealmConfiguration.Builder() - .directory(directory) - .apply { - realmKeysUtils.configureEncryption(this, getKeyAlias(userMd5)) - } - .name("crypto_store.realm") - .modules(RealmCryptoStoreModule()) - .allowWritesOnUiThread(true) - .schemaVersion(realmCryptoStoreMigration.schemaVersion) - .migration(realmCryptoStoreMigration) - .build() - } - - @JvmStatic - @Provides - @SessionScope - fun providesCryptoCoroutineScope(coroutineDispatchers: MatrixCoroutineDispatchers): CoroutineScope { - return CoroutineScope(SupervisorJob() + coroutineDispatchers.crypto) - } - - @JvmStatic - @Provides - @CryptoDatabase - fun providesClearCacheTask(@CryptoDatabase realmConfiguration: RealmConfiguration): ClearCacheTask { - return RealmClearCacheTask(realmConfiguration) - } - - @JvmStatic - @Provides - @SessionScope - fun providesCryptoAPI(retrofit: Retrofit): CryptoApi { - return retrofit.create(CryptoApi::class.java) - } - - @JvmStatic - @Provides - @SessionScope - fun providesRoomKeysAPI(retrofit: Retrofit): RoomKeysApi { - return retrofit.create(RoomKeysApi::class.java) - } - } - - @Binds - abstract fun bindCryptoService(service: DefaultCryptoService): CryptoService - - @Binds - abstract fun bindKeysBackupService(service: DefaultKeysBackupService): KeysBackupService - - @Binds - abstract fun bindDeleteDeviceTask(task: DefaultDeleteDeviceTask): DeleteDeviceTask - - @Binds - abstract fun bindGetDevicesTask(task: DefaultGetDevicesTask): GetDevicesTask - - @Binds - abstract fun bindGetDeviceInfoTask(task: DefaultGetDeviceInfoTask): GetDeviceInfoTask - - @Binds - abstract fun bindSetDeviceNameTask(task: DefaultSetDeviceNameTask): SetDeviceNameTask - - @Binds - abstract fun bindUploadKeysTask(task: DefaultUploadKeysTask): UploadKeysTask - - @Binds - abstract fun bindUploadSigningKeysTask(task: DefaultUploadSigningKeysTask): UploadSigningKeysTask - - @Binds - abstract fun bindUploadSignaturesTask(task: DefaultUploadSignaturesTask): UploadSignaturesTask - - @Binds - abstract fun bindDownloadKeysForUsersTask(task: DefaultDownloadKeysForUsers): DownloadKeysForUsersTask - - @Binds - abstract fun bindCreateKeysBackupVersionTask(task: DefaultCreateKeysBackupVersionTask): CreateKeysBackupVersionTask - - @Binds - abstract fun bindDeleteBackupTask(task: DefaultDeleteBackupTask): DeleteBackupTask - - @Binds - abstract fun bindDeleteRoomSessionDataTask(task: DefaultDeleteRoomSessionDataTask): DeleteRoomSessionDataTask - - @Binds - abstract fun bindDeleteRoomSessionsDataTask(task: DefaultDeleteRoomSessionsDataTask): DeleteRoomSessionsDataTask - - @Binds - abstract fun bindDeleteSessionsDataTask(task: DefaultDeleteSessionsDataTask): DeleteSessionsDataTask - - @Binds - abstract fun bindGetKeysBackupLastVersionTask(task: DefaultGetKeysBackupLastVersionTask): GetKeysBackupLastVersionTask - - @Binds - abstract fun bindGetKeysBackupVersionTask(task: DefaultGetKeysBackupVersionTask): GetKeysBackupVersionTask - - @Binds - abstract fun bindGetRoomSessionDataTask(task: DefaultGetRoomSessionDataTask): GetRoomSessionDataTask - - @Binds - abstract fun bindGetRoomSessionsDataTask(task: DefaultGetRoomSessionsDataTask): GetRoomSessionsDataTask - - @Binds - abstract fun bindGetSessionsDataTask(task: DefaultGetSessionsDataTask): GetSessionsDataTask - - @Binds - abstract fun bindStoreRoomSessionDataTask(task: DefaultStoreRoomSessionDataTask): StoreRoomSessionDataTask - - @Binds - abstract fun bindStoreRoomSessionsDataTask(task: DefaultStoreRoomSessionsDataTask): StoreRoomSessionsDataTask - - @Binds - abstract fun bindStoreSessionsDataTask(task: DefaultStoreSessionsDataTask): StoreSessionsDataTask - - @Binds - abstract fun bindUpdateKeysBackupVersionTask(task: DefaultUpdateKeysBackupVersionTask): UpdateKeysBackupVersionTask - - @Binds - abstract fun bindSendToDeviceTask(task: DefaultSendToDeviceTask): SendToDeviceTask - - @Binds - abstract fun bindEncryptEventTask(task: DefaultEncryptEventTask): EncryptEventTask - - @Binds - abstract fun bindSendVerificationMessageTask(task: DefaultSendVerificationMessageTask): SendVerificationMessageTask - - @Binds - abstract fun bindClaimOneTimeKeysForUsersDeviceTask(task: DefaultClaimOneTimeKeysForUsersDevice): ClaimOneTimeKeysForUsersDeviceTask - - @Binds - abstract fun bindCrossSigningService(service: DefaultCrossSigningService): CrossSigningService - - @Binds - abstract fun bindVerificationService(service: DefaultVerificationService): VerificationService - - @Binds - abstract fun bindCryptoStore(store: RealmCryptoStore): IMXCryptoStore - - @Binds - abstract fun bindCommonCryptoStore(store: RealmCryptoStore): IMXCommonCryptoStore - - @Binds - abstract fun bindSendEventTask(task: DefaultSendEventTask): SendEventTask - - @Binds - abstract fun bindInitalizeCrossSigningTask(task: DefaultInitializeCrossSigningTask): InitializeCrossSigningTask -} diff --git a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/DecryptRoomEventUseCase.kt b/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/DecryptRoomEventUseCase.kt deleted file mode 100644 index 47cc8be31e..0000000000 --- a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/DecryptRoomEventUseCase.kt +++ /dev/null @@ -1,145 +0,0 @@ -/* - * Copyright (c) 2022 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. - */ - -package org.matrix.android.sdk.internal.crypto - -import org.matrix.android.sdk.api.extensions.tryOrNull -import org.matrix.android.sdk.api.session.crypto.MXCryptoError -import org.matrix.android.sdk.api.session.crypto.model.MXEventDecryptionResult -import org.matrix.android.sdk.api.session.crypto.model.OlmDecryptionResult -import org.matrix.android.sdk.api.session.events.model.Event -import org.matrix.android.sdk.api.session.events.model.content.EncryptedEventContent -import org.matrix.android.sdk.api.session.events.model.toModel -import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore -import javax.inject.Inject - -internal class DecryptRoomEventUseCase @Inject constructor( - private val olmDevice: MXOlmDevice, - private val cryptoStore: IMXCryptoStore, - private val outgoingKeyRequestManager: OutgoingKeyRequestManager, -) { - - suspend operator fun invoke(event: Event, requestKeysOnFail: Boolean = true): MXEventDecryptionResult { - if (event.roomId.isNullOrBlank()) { - throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_FIELDS, MXCryptoError.MISSING_FIELDS_REASON) - } - - val encryptedEventContent = event.content.toModel() - ?: throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_FIELDS, MXCryptoError.MISSING_FIELDS_REASON) - - if (encryptedEventContent.senderKey.isNullOrBlank() || - encryptedEventContent.sessionId.isNullOrBlank() || - encryptedEventContent.ciphertext.isNullOrBlank()) { - throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_FIELDS, MXCryptoError.MISSING_FIELDS_REASON) - } - - try { - val olmDecryptionResult = olmDevice.decryptGroupMessage( - encryptedEventContent.ciphertext, - event.roomId, - "", - eventId = event.eventId.orEmpty(), - encryptedEventContent.sessionId, - encryptedEventContent.senderKey - ) - if (olmDecryptionResult.payload != null) { - return MXEventDecryptionResult( - clearEvent = olmDecryptionResult.payload, - senderCurve25519Key = olmDecryptionResult.senderKey, - claimedEd25519Key = olmDecryptionResult.keysClaimed?.get("ed25519"), - forwardingCurve25519KeyChain = olmDecryptionResult.forwardingCurve25519KeyChain - .orEmpty(), - messageVerificationState = olmDecryptionResult.verificationState - ) - } else { - throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_FIELDS, MXCryptoError.MISSING_FIELDS_REASON) - } - } catch (throwable: Throwable) { - if (throwable is MXCryptoError.OlmError) { - // TODO Check the value of .message - if (throwable.olmException.message == "UNKNOWN_MESSAGE_INDEX") { - // So we know that session, but it's ratcheted and we can't decrypt at that index - // Check if partially withheld - val withHeldInfo = cryptoStore.getWithHeldMegolmSession(event.roomId, encryptedEventContent.sessionId) - if (withHeldInfo != null) { - // Encapsulate as withHeld exception - throw MXCryptoError.Base( - MXCryptoError.ErrorType.KEYS_WITHHELD, - withHeldInfo.code?.value ?: "", - withHeldInfo.reason - ) - } - - throw MXCryptoError.Base( - MXCryptoError.ErrorType.UNKNOWN_MESSAGE_INDEX, - "UNKNOWN_MESSAGE_INDEX", - null - ) - } - - val reason = String.format(MXCryptoError.OLM_REASON, throwable.olmException.message) - val detailedReason = String.format(MXCryptoError.DETAILED_OLM_REASON, encryptedEventContent.ciphertext, reason) - - throw MXCryptoError.Base( - MXCryptoError.ErrorType.OLM, - reason, - detailedReason - ) - } - if (throwable is MXCryptoError.Base) { - if (throwable.errorType == MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID) { - // Check if it was withheld by sender to enrich error code - val withHeldInfo = cryptoStore.getWithHeldMegolmSession(event.roomId, encryptedEventContent.sessionId) - if (withHeldInfo != null) { - if (requestKeysOnFail) { - requestKeysForEvent(event) - } - // Encapsulate as withHeld exception - throw MXCryptoError.Base( - MXCryptoError.ErrorType.KEYS_WITHHELD, - withHeldInfo.code?.value ?: "", - withHeldInfo.reason - ) - } - - if (requestKeysOnFail) { - requestKeysForEvent(event) - } - } - } - throw throwable - } - } - - private fun requestKeysForEvent(event: Event) { - outgoingKeyRequestManager.requestKeyForEvent(event, false) - } - - suspend fun decryptAndSaveResult(event: Event) { - tryOrNull(message = "Unable to decrypt the event") { - invoke(event) - } - ?.let { result -> - event.mxDecryptionResult = OlmDecryptionResult( - payload = result.clearEvent, - senderKey = result.senderCurve25519Key, - keysClaimed = result.claimedEd25519Key?.let { mapOf("ed25519" to it) }, - forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain, - verificationState = result.messageVerificationState - ) - } - } -} diff --git a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt b/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt deleted file mode 100755 index b25c04aa9b..0000000000 --- a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt +++ /dev/null @@ -1,1457 +0,0 @@ -/* - * Copyright 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. - */ - -package org.matrix.android.sdk.internal.crypto - -import android.content.Context -import androidx.annotation.VisibleForTesting -import androidx.lifecycle.LiveData -import androidx.paging.PagedList -import com.squareup.moshi.Types -import dagger.Lazy -import kotlinx.coroutines.CancellationException -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.cancelChildren -import kotlinx.coroutines.launch -import kotlinx.coroutines.runBlocking -import kotlinx.coroutines.withContext -import org.matrix.android.sdk.api.MatrixCallback -import org.matrix.android.sdk.api.MatrixCoroutineDispatchers -import org.matrix.android.sdk.api.NoOpMatrixCallback -import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor -import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_MEGOLM -import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_OLM -import org.matrix.android.sdk.api.crypto.MXCryptoConfig -import org.matrix.android.sdk.api.extensions.tryOrNull -import org.matrix.android.sdk.api.failure.Failure -import org.matrix.android.sdk.api.listeners.ProgressListener -import org.matrix.android.sdk.api.logger.LoggerTag -import org.matrix.android.sdk.api.session.crypto.CryptoService -import org.matrix.android.sdk.api.session.crypto.GlobalCryptoConfig -import org.matrix.android.sdk.api.session.crypto.MXCryptoError -import org.matrix.android.sdk.api.session.crypto.NewSessionListener -import org.matrix.android.sdk.api.session.crypto.OutgoingKeyRequest -import org.matrix.android.sdk.api.session.crypto.crosssigning.DeviceTrustLevel -import org.matrix.android.sdk.api.session.crypto.crosssigning.KEYBACKUP_SECRET_SSSS_NAME -import org.matrix.android.sdk.api.session.crypto.crosssigning.MASTER_KEY_SSSS_NAME -import org.matrix.android.sdk.api.session.crypto.crosssigning.SELF_SIGNING_KEY_SSSS_NAME -import org.matrix.android.sdk.api.session.crypto.crosssigning.USER_SIGNING_KEY_SSSS_NAME -import org.matrix.android.sdk.api.session.crypto.keyshare.GossipingRequestListener -import org.matrix.android.sdk.api.session.crypto.model.AuditTrail -import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo -import org.matrix.android.sdk.api.session.crypto.model.DeviceInfo -import org.matrix.android.sdk.api.session.crypto.model.ImportRoomKeysResult -import org.matrix.android.sdk.api.session.crypto.model.IncomingRoomKeyRequest -import org.matrix.android.sdk.api.session.crypto.model.MXDeviceInfo -import org.matrix.android.sdk.api.session.crypto.model.MXEncryptEventContentResult -import org.matrix.android.sdk.api.session.crypto.model.MXEventDecryptionResult -import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap -import org.matrix.android.sdk.api.session.crypto.model.RoomKeyShareRequest -import org.matrix.android.sdk.api.session.crypto.model.TrailType -import org.matrix.android.sdk.api.session.events.model.Content -import org.matrix.android.sdk.api.session.events.model.Event -import org.matrix.android.sdk.api.session.events.model.EventType -import org.matrix.android.sdk.api.session.events.model.content.RoomKeyContent -import org.matrix.android.sdk.api.session.events.model.content.RoomKeyWithHeldContent -import org.matrix.android.sdk.api.session.events.model.toModel -import org.matrix.android.sdk.api.session.room.model.Membership -import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibility -import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibilityContent -import org.matrix.android.sdk.api.session.room.model.RoomMemberContent -import org.matrix.android.sdk.api.session.room.model.shouldShareHistory -import org.matrix.android.sdk.api.session.sync.model.DeviceListResponse -import org.matrix.android.sdk.api.session.sync.model.DeviceOneTimeKeysCountSyncResponse -import org.matrix.android.sdk.api.session.sync.model.SyncResponse -import org.matrix.android.sdk.api.session.sync.model.ToDeviceSyncResponse -import org.matrix.android.sdk.api.util.Optional -import org.matrix.android.sdk.internal.crypto.actions.MegolmSessionDataImporter -import org.matrix.android.sdk.internal.crypto.actions.SetDeviceVerificationAction -import org.matrix.android.sdk.internal.crypto.algorithms.IMXEncrypting -import org.matrix.android.sdk.internal.crypto.algorithms.IMXGroupEncryption -import org.matrix.android.sdk.internal.crypto.algorithms.megolm.MXMegolmEncryptionFactory -import org.matrix.android.sdk.internal.crypto.algorithms.megolm.UnRequestedForwardManager -import org.matrix.android.sdk.internal.crypto.algorithms.olm.MXOlmEncryptionFactory -import org.matrix.android.sdk.internal.crypto.crosssigning.DefaultCrossSigningService -import org.matrix.android.sdk.internal.crypto.keysbackup.DefaultKeysBackupService -import org.matrix.android.sdk.internal.crypto.model.MXKey.Companion.KEY_SIGNED_CURVE_25519_TYPE -import org.matrix.android.sdk.internal.crypto.model.SessionInfo -import org.matrix.android.sdk.internal.crypto.model.rest.KeysUploadBody -import org.matrix.android.sdk.internal.crypto.model.toRest -import org.matrix.android.sdk.internal.crypto.repository.WarnOnUnknownDeviceRepository -import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore -import org.matrix.android.sdk.internal.crypto.store.db.CryptoStoreAggregator -import org.matrix.android.sdk.internal.crypto.tasks.DeleteDeviceTask -import org.matrix.android.sdk.internal.crypto.tasks.GetDeviceInfoTask -import org.matrix.android.sdk.internal.crypto.tasks.GetDevicesTask -import org.matrix.android.sdk.internal.crypto.tasks.SetDeviceNameTask -import org.matrix.android.sdk.internal.crypto.tasks.UploadKeysTask -import org.matrix.android.sdk.internal.crypto.verification.DefaultVerificationService -import org.matrix.android.sdk.internal.crypto.verification.VerificationMessageProcessor -import org.matrix.android.sdk.internal.di.DeviceId -import org.matrix.android.sdk.internal.di.MoshiProvider -import org.matrix.android.sdk.internal.di.UserId -import org.matrix.android.sdk.internal.extensions.foldToCallback -import org.matrix.android.sdk.internal.session.SessionScope -import org.matrix.android.sdk.internal.session.StreamEventsManager -import org.matrix.android.sdk.internal.session.room.membership.LoadRoomMembersTask -import org.matrix.android.sdk.internal.session.sync.handler.CryptoSyncHandler -import org.matrix.android.sdk.internal.task.launchToCallback -import org.matrix.android.sdk.internal.util.JsonCanonicalizer -import org.matrix.android.sdk.internal.util.time.Clock -import org.matrix.olm.OlmManager -import timber.log.Timber -import java.util.concurrent.atomic.AtomicBoolean -import javax.inject.Inject -import kotlin.math.max - -/** - * A `CryptoService` class instance manages the end-to-end crypto for a session. - * - * - * Messages posted by the user are automatically redirected to CryptoService in order to be encrypted - * before sending. - * In the other hand, received events goes through CryptoService for decrypting. - * CryptoService maintains all necessary keys and their sharing with other devices required for the crypto. - * Specially, it tracks all room membership changes events in order to do keys updates. - */ - -private val loggerTag = LoggerTag("DefaultCryptoService", LoggerTag.CRYPTO) - -@SessionScope -internal class DefaultCryptoService @Inject constructor( - // Olm Manager - private val olmManager: OlmManager, - @UserId - private val userId: String, - @DeviceId - private val deviceId: String?, - private val clock: Clock, - private val myDeviceInfoHolder: Lazy, - // the crypto store - private val cryptoStore: IMXCryptoStore, - // Room encryptors store - private val roomEncryptorsStore: RoomEncryptorsStore, - // Olm device - private val olmDevice: MXOlmDevice, - // Set of parameters used to configure/customize the end-to-end crypto. - private val mxCryptoConfig: MXCryptoConfig, - // Device list manager - private val deviceListManager: DeviceListManager, - // The key backup service. - private val keysBackupService: DefaultKeysBackupService, - // - private val objectSigner: ObjectSigner, - // - private val oneTimeKeysUploader: OneTimeKeysUploader, - // - private val roomDecryptorProvider: RoomDecryptorProvider, - // The verification service. - private val verificationService: DefaultVerificationService, - - private val crossSigningService: DefaultCrossSigningService, - // - private val incomingKeyRequestManager: IncomingKeyRequestManager, - private val secretShareManager: SecretShareManager, - // - private val outgoingKeyRequestManager: OutgoingKeyRequestManager, - // Actions - private val setDeviceVerificationAction: SetDeviceVerificationAction, - private val megolmSessionDataImporter: MegolmSessionDataImporter, - private val warnOnUnknownDevicesRepository: WarnOnUnknownDeviceRepository, - // Repository - private val megolmEncryptionFactory: MXMegolmEncryptionFactory, - private val olmEncryptionFactory: MXOlmEncryptionFactory, - // Tasks - private val deleteDeviceTask: DeleteDeviceTask, - private val getDevicesTask: GetDevicesTask, - private val getDeviceInfoTask: GetDeviceInfoTask, - private val setDeviceNameTask: SetDeviceNameTask, - private val uploadKeysTask: UploadKeysTask, - private val loadRoomMembersTask: LoadRoomMembersTask, - private val cryptoSessionInfoProvider: CryptoSessionInfoProvider, - private val coroutineDispatchers: MatrixCoroutineDispatchers, - private val cryptoCoroutineScope: CoroutineScope, - private val eventDecryptor: EventDecryptor, - private val verificationMessageProcessor: VerificationMessageProcessor, - private val liveEventManager: Lazy, - private val unrequestedForwardManager: UnRequestedForwardManager, - private val cryptoSyncHandler: CryptoSyncHandler, -) : CryptoService, DeviceListManager.UserDevicesUpdateListener { - - private val isStarting = AtomicBoolean(false) - private val isStarted = AtomicBoolean(false) - - override fun name() = "kotlin-sdk" - - override fun supportsKeyWithheld() = true - override fun supportKeyRequestInspection() = true - - override fun supportsDisablingKeyGossiping() = true - - override fun supportsForwardedKeyWiththeld() = true - - override suspend fun onStateEvent(roomId: String, event: Event, cryptoStoreAggregator: CryptoStoreAggregator?) { - when (event.type) { - EventType.STATE_ROOM_ENCRYPTION -> onRoomEncryptionEvent(roomId, event) - EventType.STATE_ROOM_MEMBER -> onRoomMembershipEvent(roomId, event) - EventType.STATE_ROOM_HISTORY_VISIBILITY -> onRoomHistoryVisibilityEvent(roomId, event, cryptoStoreAggregator) - } - } - - override suspend fun onLiveEvent(roomId: String, event: Event, isInitialSync: Boolean, cryptoStoreAggregator: CryptoStoreAggregator?) { - // handle state events - if (event.isStateEvent()) { - when (event.type) { - EventType.STATE_ROOM_ENCRYPTION -> onRoomEncryptionEvent(roomId, event) - EventType.STATE_ROOM_MEMBER -> onRoomMembershipEvent(roomId, event) - EventType.STATE_ROOM_HISTORY_VISIBILITY -> onRoomHistoryVisibilityEvent(roomId, event, cryptoStoreAggregator) - } - } - - // handle verification - if (!isInitialSync) { - if (event.type != null && verificationMessageProcessor.shouldProcess(event.type)) { - withContext(coroutineDispatchers.dmVerif) { - verificationMessageProcessor.process(roomId, event) - } - } - } - } - -// val gossipingBuffer = mutableListOf() - - override suspend fun setDeviceName(deviceId: String, deviceName: String) { - setDeviceNameTask - .execute(SetDeviceNameTask.Params(deviceId, deviceName)) - cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { - downloadKeys(listOf(userId), true) - } - } - - override suspend fun deleteDevice(deviceId: String, userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor) { - deleteDevices(listOf(deviceId), userInteractiveAuthInterceptor) - } - - override suspend fun deleteDevices(deviceIds: List, userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor) { - withContext(coroutineDispatchers.crypto) { - deleteDeviceTask - .execute(DeleteDeviceTask.Params(deviceIds, userInteractiveAuthInterceptor, null)) - } - } - - override fun getCryptoVersion(context: Context, longFormat: Boolean): String { - return if (longFormat) olmManager.getDetailedVersion(context) else olmManager.version - } - - override suspend fun getMyCryptoDevice(): CryptoDeviceInfo { - return myDeviceInfoHolder.get().myDevice - } - - override suspend fun fetchDevicesList(): List { - val data = getDevicesTask - .execute(Unit) - cryptoStore.saveMyDevicesInfo(data.devices.orEmpty()) - return data.devices.orEmpty() - } - - override fun getMyDevicesInfoLive(): LiveData> { - return cryptoStore.getLiveMyDevicesInfo() - } - - override suspend fun fetchDeviceInfo(deviceId: String): DeviceInfo { - return getDeviceInfoTask.execute(GetDeviceInfoTask.Params(deviceId)) - } - - override fun getMyDevicesInfoLive(deviceId: String): LiveData> { - return cryptoStore.getLiveMyDevicesInfo(deviceId) - } - - override fun getMyDevicesInfo(): List { - return cryptoStore.getMyDevicesInfo() - } - - override suspend fun inboundGroupSessionsCount(onlyBackedUp: Boolean): Int { - return withContext(coroutineDispatchers.io) { - cryptoStore.inboundGroupSessionsCount(onlyBackedUp) - } - } - - /** - * Tell if the MXCrypto is started. - * - * @return true if the crypto is started - */ - override fun isStarted(): Boolean { - return isStarted.get() - } - - /** - * Tells if the MXCrypto is starting. - * - * @return true if the crypto is starting - */ - fun isStarting(): Boolean { - return isStarting.get() - } - - /** - * Start the crypto module. - * Device keys will be uploaded, then one time keys if there are not enough on the homeserver - * and, then, if this is the first time, this new device will be announced to all other users - * devices. - * - */ - override fun start() { - cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { - internalStart() - tryOrNull("Failed to update device list on start") { - fetchDevicesList() - } - cryptoStore.tidyUpDataBase() - deviceListManager.addListener(this@DefaultCryptoService) - } - } - - fun ensureDevice() { - cryptoCoroutineScope.launchToCallback(coroutineDispatchers.crypto, NoOpMatrixCallback()) { - // Open the store - cryptoStore.open() - - if (!cryptoStore.areDeviceKeysUploaded()) { - // Schedule upload of OTK - oneTimeKeysUploader.updateOneTimeKeyCount(0) - } - - // this can throw if no network - tryOrNull { - uploadDeviceKeys() - } - - tryOrNull { - deviceListManager.recover() - } - - oneTimeKeysUploader.maybeUploadOneTimeKeys() - // this can throw if no backup - tryOrNull { - keysBackupService.checkAndStartKeysBackup() - } - } - } - - override suspend fun onSyncWillProcess(isInitialSync: Boolean) { - withContext(coroutineDispatchers.crypto) { - if (isInitialSync) { - try { - // On initial sync, we start all our tracking from - // scratch, so mark everything as untracked. onCryptoEvent will - // be called for all e2e rooms during the processing of the sync, - // at which point we'll start tracking all the users of that room. - deviceListManager.invalidateAllDeviceLists() - // always track my devices? - deviceListManager.startTrackingDeviceList(listOf(userId)) - deviceListManager.refreshOutdatedDeviceLists() - } catch (failure: Throwable) { - Timber.tag(loggerTag.value).e(failure, "onSyncWillProcess ") - } - } - } - } - - private fun internalStart() { - if (isStarted.get() || isStarting.get()) { - return - } - isStarting.set(true) - ensureDevice() - - // Open the store - cryptoStore.open() - - isStarting.set(false) - isStarted.set(true) - } - - /** - * Close the crypto. - */ - override fun close() = runBlocking(coroutineDispatchers.crypto) { - deviceListManager.removeListener(this@DefaultCryptoService) - cryptoCoroutineScope.coroutineContext.cancelChildren(CancellationException("Closing crypto module")) - incomingKeyRequestManager.close() - outgoingKeyRequestManager.close() - unrequestedForwardManager.close() - olmDevice.release() - cryptoStore.close() - } - - // Always enabled on Matrix Android SDK2 - override fun isCryptoEnabled() = true - - /** - * @return the Keys backup Service - */ - override fun keysBackupService() = keysBackupService - - /** - * @return the VerificationService - */ - override fun verificationService() = verificationService - - override fun crossSigningService() = crossSigningService - - /** - * A sync response has been received. - * - * @param syncResponse the syncResponse - * @param cryptoStoreAggregator data aggregated during the sync response treatment to store - */ - override suspend fun onSyncCompleted(syncResponse: SyncResponse, cryptoStoreAggregator: CryptoStoreAggregator) { -// if (syncResponse.deviceLists != null) { -// deviceListManager.handleDeviceListsChanges(syncResponse.deviceLists.changed, syncResponse.deviceLists.left) -// } -// if (syncResponse.deviceOneTimeKeysCount != null) { -// val currentCount = syncResponse.deviceOneTimeKeysCount.signedCurve25519 ?: 0 -// oneTimeKeysUploader.updateOneTimeKeyCount(currentCount) -// } - cryptoStore.storeData(cryptoStoreAggregator) - // unwedge if needed - try { - eventDecryptor.unwedgeDevicesIfNeeded() - } catch (failure: Throwable) { - Timber.tag(loggerTag.value).w("unwedgeDevicesIfNeeded failed") - } - - // There is a limit of to_device events returned per sync. - // If we are in a case of such limited to_device sync we can't try to generate/upload - // new otk now, because there might be some pending olm pre-key to_device messages that would fail if we rotate - // the old otk too early. In this case we want to wait for the pending to_device before doing anything - // As per spec: - // If there is a large queue of send-to-device messages, the server should limit the number sent in each /sync response. - // 100 messages is recommended as a reasonable limit. - // The limit is not part of the spec, so it's probably safer to handle that when there are no more to_device ( so we are sure - // that there are no pending to_device - val toDevices = syncResponse.toDevice?.events.orEmpty() - if (isStarted() && toDevices.isEmpty()) { - // Make sure we process to-device messages before generating new one-time-keys #2782 - deviceListManager.refreshOutdatedDeviceLists() - // The presence of device_unused_fallback_key_types indicates that the server supports fallback keys. - // If there's no unused signed_curve25519 fallback key we need a new one. - if (syncResponse.deviceUnusedFallbackKeyTypes != null && - // Generate a fallback key only if the server does not already have an unused fallback key. - !syncResponse.deviceUnusedFallbackKeyTypes.contains(KEY_SIGNED_CURVE_25519_TYPE)) { - oneTimeKeysUploader.needsNewFallback() - } - - oneTimeKeysUploader.maybeUploadOneTimeKeys() - } - - // Process pending key requests - try { - if (toDevices.isEmpty()) { - // this is not blocking - outgoingKeyRequestManager.requireProcessAllPendingKeyRequests() - } else { - Timber.tag(loggerTag.value) - .w("Don't process key requests yet as there might be more to_device to catchup") - } - } catch (failure: Throwable) { - // just for safety but should not throw - Timber.tag(loggerTag.value).w("failed to process pending request") - } - - try { - incomingKeyRequestManager.processIncomingRequests() - } catch (failure: Throwable) { - // just for safety but should not throw - Timber.tag(loggerTag.value).w("failed to process incoming room key requests") - } - - unrequestedForwardManager.postSyncProcessParkedKeysIfNeeded(clock.epochMillis()) { events -> - cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { - events.forEach { - onRoomKeyEvent(it, true) - } - } - } - } - - override fun logDbUsageInfo() { - // - } - - /** - * Find a device by curve25519 identity key. - * - * @param senderKey the curve25519 key to match. - * @param algorithm the encryption algorithm. - * @return the device info, or null if not found / unsupported algorithm / crypto released - */ - override suspend fun deviceWithIdentityKey(userId: String, senderKey: String, algorithm: String): CryptoDeviceInfo? { - return if (algorithm != MXCRYPTO_ALGORITHM_MEGOLM && algorithm != MXCRYPTO_ALGORITHM_OLM) { - // We only deal in olm keys - null - } else { - withContext(coroutineDispatchers.io) { - cryptoStore.deviceWithIdentityKey(userId, senderKey) - } - } - } - - /** - * Provides the device information for a user id and a device Id. - * - * @param userId the user id - * @param deviceId the device id - */ - override suspend fun getCryptoDeviceInfo(userId: String, deviceId: String?): CryptoDeviceInfo? { - return if (userId.isNotEmpty() && !deviceId.isNullOrEmpty()) { - withContext(coroutineDispatchers.io) { - cryptoStore.getUserDevice(userId, deviceId) - } - } else { - null - } - } - -// override fun getCryptoDeviceInfo(deviceId: String, callback: MatrixCallback) { -// getDeviceInfoTask -// .configureWith(GetDeviceInfoTask.Params(deviceId)) { -// this.executionThread = TaskThread.CRYPTO -// this.callback = callback -// } -// .executeBy(taskExecutor) -// } - - override suspend fun getCryptoDeviceInfo(userId: String): List { - return cryptoStore.getUserDeviceList(userId).orEmpty() - } -// -// override fun getCryptoDeviceInfoFlow(userId: String): Flow> { -// return cryptoStore.getUserDeviceListFlow(userId) -// } - - override fun getLiveCryptoDeviceInfo(): LiveData> { - return cryptoStore.getLiveDeviceList() - } - - override fun getLiveCryptoDeviceInfoWithId(deviceId: String): LiveData> { - return cryptoStore.getLiveDeviceWithId(deviceId) - } - - override fun getLiveCryptoDeviceInfo(userId: String): LiveData> { - return cryptoStore.getLiveDeviceList(userId) - } - - override fun getLiveCryptoDeviceInfo(userIds: List): LiveData> { - return cryptoStore.getLiveDeviceList(userIds) - } - - /** - * Set the devices as known. - * - * @param devices the devices. Note that the verified member of the devices in this list will not be updated by this method. - * @param callback the asynchronous callback - */ - fun setDevicesKnown(devices: List, callback: MatrixCallback?) { - // build a devices map - val devicesIdListByUserId = devices.groupBy({ it.userId }, { it.deviceId }) - - for ((userId, deviceIds) in devicesIdListByUserId) { - val storedDeviceIDs = cryptoStore.getUserDevices(userId) - - // sanity checks - if (null != storedDeviceIDs) { - var isUpdated = false - - deviceIds.forEach { deviceId -> - val device = storedDeviceIDs[deviceId] - - // assume if the device is either verified or blocked - // it means that the device is known - if (device?.isUnknown == true) { - device.trustLevel = DeviceTrustLevel(crossSigningVerified = false, locallyVerified = false) - isUpdated = true - } - } - - if (isUpdated) { - cryptoStore.storeUserDevices(userId, storedDeviceIDs) - } - } - } - - callback?.onSuccess(Unit) - } - - /** - * Update the blocked/verified state of the given device. - * - * @param trustLevel the new trust level - * @param userId the owner of the device - * @param deviceId the unique identifier for the device. - */ - override suspend fun setDeviceVerification(trustLevel: DeviceTrustLevel, userId: String, deviceId: String) { - setDeviceVerificationAction.handle(trustLevel, userId, deviceId) - } - - /** - * Configure a room to use encryption. - * - * @param roomId the room id to enable encryption in. - * @param algorithm the encryption config for the room. - * @param inhibitDeviceQuery true to suppress device list query for users in the room (for now) - * @param membersId list of members to start tracking their devices - * @return true if the operation succeeds. - */ - private suspend fun setEncryptionInRoom( - roomId: String, - algorithm: String?, - inhibitDeviceQuery: Boolean, - membersId: List - ): Boolean { - // If we already have encryption in this room, we should ignore this event - // (for now at least. Maybe we should alert the user somehow?) - val existingAlgorithm = cryptoStore.getRoomAlgorithm(roomId) - - if (existingAlgorithm == algorithm) { - // ignore - Timber.tag(loggerTag.value).e("setEncryptionInRoom() : Ignoring m.room.encryption for same alg ($algorithm) in $roomId") - return false - } - - val encryptingClass = MXCryptoAlgorithms.hasEncryptorClassForAlgorithm(algorithm) - - // Always store even if not supported - cryptoStore.storeRoomAlgorithm(roomId, algorithm) - - if (!encryptingClass) { - Timber.tag(loggerTag.value).e("setEncryptionInRoom() : Unable to encrypt room $roomId with $algorithm") - return false - } - - val alg: IMXEncrypting? = when (algorithm) { - MXCRYPTO_ALGORITHM_MEGOLM -> megolmEncryptionFactory.create(roomId) - MXCRYPTO_ALGORITHM_OLM -> olmEncryptionFactory.create(roomId) - else -> null - } - - if (alg != null) { - roomEncryptorsStore.put(roomId, alg) - } - - // if encryption was not previously enabled in this room, we will have been - // ignoring new device events for these users so far. We may well have - // up-to-date lists for some users, for instance if we were sharing other - // e2e rooms with them, so there is room for optimisation here, but for now - // we just invalidate everyone in the room. - if (null == existingAlgorithm) { - Timber.tag(loggerTag.value).d("Enabling encryption in $roomId for the first time; invalidating device lists for all users therein") - - val userIds = ArrayList(membersId) - - deviceListManager.startTrackingDeviceList(userIds) - - if (!inhibitDeviceQuery) { - deviceListManager.refreshOutdatedDeviceLists() - } - } - - return true - } - - /** - * Tells if a room is encrypted with MXCRYPTO_ALGORITHM_MEGOLM. - * - * @param roomId the room id - * @return true if the room is encrypted with algorithm MXCRYPTO_ALGORITHM_MEGOLM - */ - override fun isRoomEncrypted(roomId: String): Boolean { - return cryptoSessionInfoProvider.isRoomEncrypted(roomId) - } - - /** - * @return the stored device keys for a user. - */ - override suspend fun getUserDevices(userId: String): List { - return withContext(coroutineDispatchers.io) { - cryptoStore.getUserDevices(userId)?.values?.toList().orEmpty() - } - } - - private fun isEncryptionEnabledForInvitedUser(): Boolean { - return mxCryptoConfig.enableEncryptionForInvitedMembers - } - - override fun getEncryptionAlgorithm(roomId: String): String? { - return cryptoStore.getRoomAlgorithm(roomId) - } - - /** - * Determine whether we should encrypt messages for invited users in this room. - *

- * Check here whether the invited members are allowed to read messages in the room history - * from the point they were invited onwards. - * - * @return true if we should encrypt messages for invited users. - */ - override fun shouldEncryptForInvitedMembers(roomId: String): Boolean { - return cryptoStore.shouldEncryptForInvitedMembers(roomId) - } - - /** - * Encrypt an event content according to the configuration of the room. - * - * @param eventContent the content of the event. - * @param eventType the type of the event. - * @param roomId the room identifier the event will be sent. - * @param callback the asynchronous callback - */ - override suspend fun encryptEventContent( - eventContent: Content, - eventType: String, - roomId: String, - ): MXEncryptEventContentResult { - // moved to crypto scope to have uptodate values - return withContext(coroutineDispatchers.crypto) { - val userIds = getRoomUserIds(roomId) - var alg = roomEncryptorsStore.get(roomId) - if (alg == null) { - val algorithm = getEncryptionAlgorithm(roomId) - if (algorithm != null) { - if (setEncryptionInRoom(roomId, algorithm, false, userIds)) { - alg = roomEncryptorsStore.get(roomId) - } - } - } - val safeAlgorithm = alg - if (safeAlgorithm != null) { - val t0 = clock.epochMillis() - Timber.tag(loggerTag.value).v("encryptEventContent() starts") - val content = safeAlgorithm.encryptEventContent(eventContent, eventType, userIds) - Timber.tag(loggerTag.value).v("## CRYPTO | encryptEventContent() : succeeds after ${clock.epochMillis() - t0} ms") - return@withContext MXEncryptEventContentResult(content, EventType.ENCRYPTED) - } else { - val algorithm = getEncryptionAlgorithm(roomId) - val reason = String.format( - MXCryptoError.UNABLE_TO_ENCRYPT_REASON, - algorithm ?: MXCryptoError.NO_MORE_ALGORITHM_REASON - ) - Timber.tag(loggerTag.value).e("encryptEventContent() : failed $reason") - throw Failure.CryptoError(MXCryptoError.Base(MXCryptoError.ErrorType.UNABLE_TO_ENCRYPT, reason)) - } - } - } - - override fun discardOutboundSession(roomId: String) { - cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { - val roomEncryptor = roomEncryptorsStore.get(roomId) - if (roomEncryptor is IMXGroupEncryption) { - roomEncryptor.discardSessionKey() - } else { - Timber.tag(loggerTag.value).e("discardOutboundSession() for:$roomId: Unable to handle IMXGroupEncryption") - } - } - } - - /** - * Decrypt an event. - * - * @param event the raw event. - * @param timeline the id of the timeline where the event is decrypted. It is used to prevent replay attack. - * @return the MXEventDecryptionResult data, or throw in case of error - */ - @Throws(MXCryptoError::class) - override suspend fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult { - return internalDecryptEvent(event, timeline) - } - - /** - * Decrypt an event. - * - * @param event the raw event. - * @param timeline the id of the timeline where the event is decrypted. It is used to prevent replay attack. - * @return the MXEventDecryptionResult data, or null in case of error - */ - @Throws(MXCryptoError::class) - private suspend fun internalDecryptEvent(event: Event, timeline: String): MXEventDecryptionResult { - return withContext(coroutineDispatchers.crypto) { eventDecryptor.decryptEvent(event, timeline) } - } - - /** - * Reset replay attack data for the given timeline. - * - * @param timelineId the timeline id - */ - fun resetReplayAttackCheckInTimeline(timelineId: String) { - olmDevice.resetReplayAttackCheckInTimeline(timelineId) - } - - /** - * Handle the 'toDevice' event. - * - * @param event the event - */ - fun onToDeviceEvent(event: Event) { - // event have already been decrypted - cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { - when (event.getClearType()) { - EventType.ROOM_KEY, EventType.FORWARDED_ROOM_KEY -> { - // Keys are imported directly, not waiting for end of sync - onRoomKeyEvent(event) - } - EventType.REQUEST_SECRET -> { - secretShareManager.handleSecretRequest(event) - } - EventType.ROOM_KEY_REQUEST -> { - event.getClearContent().toModel()?.let { req -> - // We'll always get these because we send room key requests to - // '*' (ie. 'all devices') which includes the sending device, - // so ignore requests from ourself because apart from it being - // very silly, it won't work because an Olm session cannot send - // messages to itself. - if (req.requestingDeviceId != deviceId) { // ignore self requests - event.senderId?.let { incomingKeyRequestManager.addNewIncomingRequest(it, req) } - } - } - } - EventType.SEND_SECRET -> { - onSecretSendReceived(event) - } - in EventType.ROOM_KEY_WITHHELD.values -> { - onKeyWithHeldReceived(event) - } - else -> { - // ignore - } - } - } - liveEventManager.get().dispatchOnLiveToDevice(event) - } - - /** - * Handle a key event. - * - * @param event the key event. - * @param acceptUnrequested, if true it will force to accept unrequested keys. - */ - private suspend fun onRoomKeyEvent(event: Event, acceptUnrequested: Boolean = false) { - val roomKeyContent = event.getDecryptedContent().toModel() ?: return - Timber.tag(loggerTag.value) - .i("onRoomKeyEvent(f:$acceptUnrequested) from: ${event.senderId} type<${event.getClearType()}> , session<${roomKeyContent.sessionId}>") - if (roomKeyContent.roomId.isNullOrEmpty() || roomKeyContent.algorithm.isNullOrEmpty()) { - Timber.tag(loggerTag.value).e("onRoomKeyEvent() : missing fields") - return - } - val alg = roomDecryptorProvider.getOrCreateRoomDecryptor(roomKeyContent.roomId, roomKeyContent.algorithm) - if (alg == null) { - Timber.tag(loggerTag.value).e("GOSSIP onRoomKeyEvent() : Unable to handle keys for ${roomKeyContent.algorithm}") - return - } - alg.onRoomKeyEvent(event, keysBackupService, acceptUnrequested) - } - - private fun onKeyWithHeldReceived(event: Event) { - val withHeldContent = event.getClearContent().toModel() ?: return Unit.also { - Timber.tag(loggerTag.value).i("Malformed onKeyWithHeldReceived() : missing fields") - } - val senderId = event.senderId ?: return Unit.also { - Timber.tag(loggerTag.value).i("Malformed onKeyWithHeldReceived() : missing fields") - } - withHeldContent.sessionId ?: return - withHeldContent.algorithm ?: return - withHeldContent.roomId ?: return - withHeldContent.senderKey ?: return - outgoingKeyRequestManager.onRoomKeyWithHeld( - sessionId = withHeldContent.sessionId, - algorithm = withHeldContent.algorithm, - roomId = withHeldContent.roomId, - senderKey = withHeldContent.senderKey, - fromDevice = withHeldContent.fromDevice, - event = Event( - type = EventType.ROOM_KEY_WITHHELD.stable, - senderId = senderId, - content = event.getClearContent() - ) - ) - } - - private suspend fun onSecretSendReceived(event: Event) { - secretShareManager.onSecretSendReceived(event) { secretName, secretValue -> - handleSDKLevelGossip(secretName, secretValue) - } - } - - /** - * Returns true if handled by SDK, otherwise should be sent to application layer. - */ - private fun handleSDKLevelGossip( - secretName: String?, - secretValue: String - ): Boolean { - return when (secretName) { - MASTER_KEY_SSSS_NAME -> { - cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { - crossSigningService.onSecretMSKGossip(secretValue) - } - true - } - SELF_SIGNING_KEY_SSSS_NAME -> { - cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { - crossSigningService.onSecretSSKGossip(secretValue) - } - true - } - USER_SIGNING_KEY_SSSS_NAME -> { - cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { - crossSigningService.onSecretUSKGossip(secretValue) - } - true - } - KEYBACKUP_SECRET_SSSS_NAME -> { - cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { - keysBackupService.onSecretKeyGossip(secretValue) - } - true - } - else -> false - } - } - - /** - * Handle an m.room.encryption event. - * - * @param roomId the room Id - * @param event the encryption event. - */ - private suspend fun onRoomEncryptionEvent(roomId: String, event: Event) { - if (!event.isStateEvent()) { - // Ignore - Timber.tag(loggerTag.value).w("Invalid encryption event") - return - } - withContext(coroutineDispatchers.io) { - val userIds = getRoomUserIds(roomId) - setEncryptionInRoom(roomId, event.content?.get("algorithm")?.toString(), true, userIds) - } - } - - private fun getRoomUserIds(roomId: String): List { - val encryptForInvitedMembers = isEncryptionEnabledForInvitedUser() && - shouldEncryptForInvitedMembers(roomId) - return cryptoSessionInfoProvider.getRoomUserIds(roomId, encryptForInvitedMembers) - } - - /** - * Handle a change in the membership state of a member of a room. - * - * @param roomId the room Id - * @param event the membership event causing the change - */ - private suspend fun onRoomMembershipEvent(roomId: String, event: Event) { - // because the encryption event can be after the join/invite in the same batch - event.stateKey?.let { _ -> - val roomMember: RoomMemberContent? = event.content.toModel() - val membership = roomMember?.membership - if (membership == Membership.INVITE) { - unrequestedForwardManager.onInviteReceived(roomId, event.senderId.orEmpty(), clock.epochMillis()) - } - } - roomEncryptorsStore.get(roomId) ?: /* No encrypting in this room */ return - withContext(coroutineDispatchers.io) { - event.stateKey?.let { userId -> - val roomMember: RoomMemberContent? = event.content.toModel() - val membership = roomMember?.membership - if (membership == Membership.JOIN) { - // make sure we are tracking the deviceList for this user. - deviceListManager.startTrackingDeviceList(listOf(userId)) - } else if (membership == Membership.INVITE && - shouldEncryptForInvitedMembers(roomId) && - isEncryptionEnabledForInvitedUser()) { - // track the deviceList for this invited user. - // Caution: there's a big edge case here in that federated servers do not - // know what other servers are in the room at the time they've been invited. - // They therefore will not send device updates if a user logs in whilst - // their state is invite. - deviceListManager.startTrackingDeviceList(listOf(userId)) - } - } - } - } - - private suspend fun onRoomHistoryVisibilityEvent(roomId: String, event: Event, cryptoStoreAggregator: CryptoStoreAggregator?) { - if (!event.isStateEvent()) return - val eventContent = event.content.toModel() - val historyVisibility = eventContent?.historyVisibility - withContext(coroutineDispatchers.io) { - if (historyVisibility == null) { - if (cryptoStoreAggregator != null) { - cryptoStoreAggregator.setShouldShareHistoryData[roomId] = false - } else { - cryptoStore.setShouldShareHistory(roomId, false) - } - } else { - if (cryptoStoreAggregator != null) { - cryptoStoreAggregator.setShouldEncryptForInvitedMembersData[roomId] = historyVisibility != RoomHistoryVisibility.JOINED - cryptoStoreAggregator.setShouldShareHistoryData[roomId] = historyVisibility.shouldShareHistory() - } else { - cryptoStore.setShouldEncryptForInvitedMembers(roomId, historyVisibility != RoomHistoryVisibility.JOINED) - cryptoStore.setShouldShareHistory(roomId, historyVisibility.shouldShareHistory()) - } - } - } - } - - /** - * Upload my user's device keys. - */ - private suspend fun uploadDeviceKeys() { - if (cryptoStore.areDeviceKeysUploaded()) { - Timber.tag(loggerTag.value).d("Keys already uploaded, nothing to do") - return - } - // Prepare the device keys data to send - // Sign it - val myCryptoDevice = getMyCryptoDevice() - val canonicalJson = JsonCanonicalizer.getCanonicalJson(Map::class.java, myCryptoDevice.signalableJSONDictionary()) - var rest = myCryptoDevice.toRest() - - rest = rest.copy( - signatures = objectSigner.signObject(canonicalJson) - ) - - val keyUploadBody = KeysUploadBody( - deviceKeys = rest, - ) - val uploadDeviceKeysParams = UploadKeysTask.Params(keyUploadBody) - uploadKeysTask.execute(uploadDeviceKeysParams) - - cryptoStore.setDeviceKeysUploaded(true) - } - - override suspend fun receiveSyncChanges( - toDevice: ToDeviceSyncResponse?, - deviceChanges: DeviceListResponse?, - keyCounts: DeviceOneTimeKeysCountSyncResponse?, - deviceUnusedFallbackKeyTypes: List? - ) { - withContext(coroutineDispatchers.crypto) { - deviceListManager.handleDeviceListsChanges(deviceChanges?.changed.orEmpty(), deviceChanges?.left.orEmpty()) - if (keyCounts != null) { - val currentCount = keyCounts.signedCurve25519 ?: 0 - oneTimeKeysUploader.updateOneTimeKeyCount(currentCount) - } - - cryptoSyncHandler.handleToDevice(toDevice?.events.orEmpty()) - } - } - - /** - * Export the crypto keys. - * - * @param password the password - * @return the exported keys - */ - override suspend fun exportRoomKeys(password: String): ByteArray { - return exportRoomKeys(password, MXMegolmExportEncryption.DEFAULT_ITERATION_COUNT) - } - - /** - * Export the crypto keys. - * - * @param password the password - * @param anIterationCount the encryption iteration count (0 means no encryption) - */ - private suspend fun exportRoomKeys(password: String, anIterationCount: Int): ByteArray { - return withContext(coroutineDispatchers.crypto) { - val iterationCount = max(0, anIterationCount) - - val exportedSessions = cryptoStore.getInboundGroupSessions().mapNotNull { it.exportKeys() } - - val adapter = MoshiProvider.providesMoshi() - .adapter(List::class.java) - - MXMegolmExportEncryption.encryptMegolmKeyFile(adapter.toJson(exportedSessions), password, iterationCount) - } - } - - /** - * Import the room keys. - * - * @param roomKeysAsArray the room keys as array. - * @param password the password - * @param progressListener the progress listener - * @return the result ImportRoomKeysResult - */ - override suspend fun importRoomKeys( - roomKeysAsArray: ByteArray, - password: String, - progressListener: ProgressListener? - ): ImportRoomKeysResult { - return withContext(coroutineDispatchers.crypto) { - Timber.tag(loggerTag.value).v("importRoomKeys starts") - - val t0 = clock.epochMillis() - val roomKeys = MXMegolmExportEncryption.decryptMegolmKeyFile(roomKeysAsArray, password) - val t1 = clock.epochMillis() - - Timber.tag(loggerTag.value).v("importRoomKeys : decryptMegolmKeyFile done in ${t1 - t0} ms") - - val importedSessions = MoshiProvider.providesMoshi() - .adapter>(Types.newParameterizedType(List::class.java, MegolmSessionData::class.java)) - .fromJson(roomKeys) - - val t2 = clock.epochMillis() - - Timber.tag(loggerTag.value).v("importRoomKeys : JSON parsing ${t2 - t1} ms") - - if (importedSessions == null) { - throw Exception("Error") - } - - megolmSessionDataImporter.handle( - megolmSessionsData = importedSessions, - fromBackup = false, - progressListener = progressListener - ) - } - } - - /** - * Update the warn status when some unknown devices are detected. - * - * @param warn true to warn when some unknown devices are detected. - */ - override fun setWarnOnUnknownDevices(warn: Boolean) { - warnOnUnknownDevicesRepository.setWarnOnUnknownDevices(warn) - } - - /** - * Check if the user ids list have some unknown devices. - * A success means there is no unknown devices. - * If there are some unknown devices, a MXCryptoError.UnknownDevice exception is triggered. - * - * @param userIds the user ids list - * @param callback the asynchronous callback. - */ - fun checkUnknownDevices(userIds: List, callback: MatrixCallback) { - // force the refresh to ensure that the devices list is up-to-date - cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { - runCatching { - val keys = deviceListManager.downloadKeys(userIds, true) - val unknownDevices = getUnknownDevices(keys) - if (unknownDevices.map.isNotEmpty()) { - // trigger an an unknown devices exception - throw Failure.CryptoError(MXCryptoError.UnknownDevice(unknownDevices)) - } - }.foldToCallback(callback) - } - } - - override suspend fun downloadKeysIfNeeded(userIds: List, forceDownload: Boolean): MXUsersDevicesMap { - return deviceListManager.downloadKeys(userIds, forceDownload) - } - - override suspend fun getCryptoDeviceInfoList(userId: String): List { - return cryptoStore.getUserDeviceList(userId).orEmpty() - } -// -// fun getLiveCryptoDeviceInfoList(userId: String): Flow> { -// cryptoStore.getLiveDeviceList(userId).asFlow() -// } -// -// fun getLiveCryptoDeviceInfoList(userIds: List): Flow> { -// -// } - - /** - * Set the global override for whether the client should ever send encrypted - * messages to unverified devices. - * If false, it can still be overridden per-room. - * If true, it overrides the per-room settings. - * - * @param block true to unilaterally blacklist all - */ - override fun setGlobalBlacklistUnverifiedDevices(block: Boolean) { - cryptoStore.setGlobalBlacklistUnverifiedDevices(block) - } - - override fun enableKeyGossiping(enable: Boolean) { - cryptoStore.enableKeyGossiping(enable) - } - - override fun isKeyGossipingEnabled() = cryptoStore.isKeyGossipingEnabled() - - override fun isShareKeysOnInviteEnabled() = cryptoStore.isShareKeysOnInviteEnabled() - - override fun supportsShareKeysOnInvite() = true - - override fun enableShareKeyOnInvite(enable: Boolean) = cryptoStore.enableShareKeyOnInvite(enable) - - /** - * Tells whether the client should ever send encrypted messages to unverified devices. - * The default value is false. - * This function must be called in the getEncryptingThreadHandler() thread. - * - * @return true to unilaterally blacklist all unverified devices. - */ - override fun getGlobalBlacklistUnverifiedDevices(): Boolean { - return cryptoStore.getGlobalBlacklistUnverifiedDevices() - } - - override fun getLiveGlobalCryptoConfig(): LiveData { - return cryptoStore.getLiveGlobalCryptoConfig() - } - - /** - * Tells whether the client should encrypt messages only for the verified devices - * in this room. - * The default value is false. - * - * @param roomId the room id - * @return true if the client should encrypt messages only for the verified devices. - */ - override fun isRoomBlacklistUnverifiedDevices(roomId: String?): Boolean { - return roomId?.let { cryptoStore.getBlockUnverifiedDevices(roomId) } - ?: false - } - - /** - * A live status regarding sharing keys for unverified devices in this room. - * - * @return Live status - */ - override fun getLiveBlockUnverifiedDevices(roomId: String): LiveData { - return cryptoStore.getLiveBlockUnverifiedDevices(roomId) - } - - /** - * Add this room to the ones which don't encrypt messages to unverified devices. - * - * @param roomId the room id - * @param block if true will block sending keys to unverified devices - */ - override fun setRoomBlockUnverifiedDevices(roomId: String, block: Boolean) { - cryptoStore.blockUnverifiedDevicesInRoom(roomId, block) - } - - /** - * Remove this room to the ones which don't encrypt messages to unverified devices. - * - * @param roomId the room id - */ - override fun setRoomUnBlockUnverifiedDevices(roomId: String) { - setRoomBlockUnverifiedDevices(roomId, false) - } - - /** - * Re request the encryption keys required to decrypt an event. - * - * @param event the event to decrypt again. - */ - override suspend fun reRequestRoomKeyForEvent(event: Event) { - outgoingKeyRequestManager.requestKeyForEvent(event, true) - } - - suspend fun requestRoomKeyForEvent(event: Event) { - outgoingKeyRequestManager.requestKeyForEvent(event, false) - } - - /** - * Add a GossipingRequestListener listener. - * - * @param listener listener - */ - override fun addRoomKeysRequestListener(listener: GossipingRequestListener) { - incomingKeyRequestManager.addRoomKeysRequestListener(listener) - secretShareManager.addListener(listener) - } - - /** - * Add a GossipingRequestListener listener. - * - * @param listener listener - */ - override fun removeRoomKeysRequestListener(listener: GossipingRequestListener) { - incomingKeyRequestManager.removeRoomKeysRequestListener(listener) - secretShareManager.removeListener(listener) - } - - /** - * Provides the list of unknown devices. - * - * @param devicesInRoom the devices map - * @return the unknown devices map - */ - private fun getUnknownDevices(devicesInRoom: MXUsersDevicesMap): MXUsersDevicesMap { - val unknownDevices = MXUsersDevicesMap() - val userIds = devicesInRoom.userIds - for (userId in userIds) { - devicesInRoom.getUserDeviceIds(userId)?.forEach { deviceId -> - devicesInRoom.getObject(userId, deviceId) - ?.takeIf { it.isUnknown } - ?.let { - unknownDevices.setObject(userId, deviceId, it) - } - } - } - - return unknownDevices - } - - suspend fun downloadKeys(userIds: List, forceDownload: Boolean): MXUsersDevicesMap { - return deviceListManager.downloadKeys(userIds, forceDownload) - } - - override fun addNewSessionListener(newSessionListener: NewSessionListener) { - roomDecryptorProvider.addNewSessionListener(newSessionListener) - } - - override fun removeSessionListener(listener: NewSessionListener) { - roomDecryptorProvider.removeSessionListener(listener) - } - - override fun onUsersDeviceUpdate(userIds: List) { - cryptoSessionInfoProvider.markMessageVerificationStateAsDirty(userIds) - } -/* ========================================================================================== - * DEBUG INFO - * ========================================================================================== */ - - override fun toString(): String { - return "DefaultCryptoService of $userId ($deviceId)" - } - - override fun getOutgoingRoomKeyRequests(): List { - return cryptoStore.getOutgoingRoomKeyRequests() - } - - override fun getOutgoingRoomKeyRequestsPaged(): LiveData> { - return cryptoStore.getOutgoingRoomKeyRequestsPaged() - } - - override fun getIncomingRoomKeyRequests(): List { - return cryptoStore.getGossipingEvents() - .mapNotNull { - IncomingRoomKeyRequest.fromEvent(it) - } - } - - override fun getIncomingRoomKeyRequestsPaged(): LiveData> { - return cryptoStore.getGossipingEventsTrail(TrailType.IncomingKeyRequest) { - IncomingRoomKeyRequest.fromEvent(it) - ?: IncomingRoomKeyRequest(localCreationTimestamp = 0L) - } - } - - /** - * If you registered a `GossipingRequestListener`, you will be notified of key request - * that was not accepted by the SDK. You can call back this manually to accept anyhow. - */ - override suspend fun manuallyAcceptRoomKeyRequest(request: IncomingRoomKeyRequest) { - incomingKeyRequestManager.manuallyAcceptRoomKeyRequest(request) - } - - override fun getGossipingEventsTrail(): LiveData> { - return cryptoStore.getGossipingEventsTrail() - } - - override fun getGossipingEvents(): List { - return cryptoStore.getGossipingEvents() - } - - override fun getSharedWithInfo(roomId: String?, sessionId: String): MXUsersDevicesMap { - return cryptoStore.getSharedWithInfo(roomId, sessionId) - } - - override fun getWithHeldMegolmSession(roomId: String, sessionId: String): RoomKeyWithHeldContent? { - return cryptoStore.getWithHeldMegolmSession(roomId, sessionId) - } - - override suspend fun prepareToEncrypt(roomId: String) { - withContext(coroutineDispatchers.crypto) { - Timber.tag(loggerTag.value).d("prepareToEncrypt() roomId:$roomId Check room members up to date") - // Ensure to load all room members - try { - loadRoomMembersTask.execute(LoadRoomMembersTask.Params(roomId)) - } catch (failure: Throwable) { - Timber.tag(loggerTag.value).e("prepareToEncrypt() : Failed to load room members") - // we probably shouldn't block sending on that (but questionable) - // but some members won't be able to decrypt - } - - val userIds = getRoomUserIds(roomId) - val alg = roomEncryptorsStore.get(roomId) - ?: getEncryptionAlgorithm(roomId) - ?.let { setEncryptionInRoom(roomId, it, false, userIds) } - ?.let { roomEncryptorsStore.get(roomId) } - - if (alg == null) { - val reason = String.format(MXCryptoError.UNABLE_TO_ENCRYPT_REASON, MXCryptoError.NO_MORE_ALGORITHM_REASON) - Timber.tag(loggerTag.value).e("prepareToEncrypt() : $reason") - throw IllegalArgumentException("Missing algorithm") - } - - (alg as? IMXGroupEncryption)?.preshareKey(userIds) - } - } - - override suspend fun sendSharedHistoryKeys(roomId: String, userId: String, sessionInfoSet: Set?) { - deviceListManager.downloadKeys(listOf(userId), false) - val userDevices = cryptoStore.getUserDeviceList(userId) - val sessionToShare = sessionInfoSet.orEmpty().mapNotNull { sessionInfo -> - // Get inbound session from sessionId and sessionKey - withContext(coroutineDispatchers.crypto) { - olmDevice.getInboundGroupSession( - sessionId = sessionInfo.sessionId, - senderKey = sessionInfo.senderKey, - roomId = roomId - ).takeIf { it.wrapper.sessionData.sharedHistory } - } - } - - userDevices?.forEach { deviceInfo -> - // Lets share the provided inbound sessions for every user device - sessionToShare.forEach { inboundGroupSession -> - val encryptor = roomEncryptorsStore.get(roomId) - encryptor?.shareHistoryKeysWithDevice(inboundGroupSession, deviceInfo) - Timber.i("## CRYPTO | Sharing inbound session") - } - } - } - - override fun onE2ERoomMemberLoadedFromServer(roomId: String) { - cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { - val userIds = getRoomUserIds(roomId) - // Because of LL we might want to update tracked users - deviceListManager.startTrackingDeviceList(userIds) - } - } - - /* ========================================================================================== - * For test only - * ========================================================================================== */ - - @VisibleForTesting - val cryptoStoreForTesting = cryptoStore - - @VisibleForTesting - val olmDeviceForTest = olmDevice - - companion object { - const val CRYPTO_MIN_FORCE_SESSION_PERIOD_MILLIS = 3_600_000 // one hour - } -} diff --git a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/DeviceListManager.kt b/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/DeviceListManager.kt deleted file mode 100755 index d7703e7426..0000000000 --- a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/DeviceListManager.kt +++ /dev/null @@ -1,603 +0,0 @@ -/* - * Copyright 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. - */ - -package org.matrix.android.sdk.internal.crypto - -import kotlinx.coroutines.CancellationException -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import org.matrix.android.sdk.api.MatrixConfiguration -import org.matrix.android.sdk.api.MatrixCoroutineDispatchers -import org.matrix.android.sdk.api.MatrixPatterns -import org.matrix.android.sdk.api.auth.data.Credentials -import org.matrix.android.sdk.api.extensions.measureMetric -import org.matrix.android.sdk.api.metrics.DownloadDeviceKeysMetricsPlugin -import org.matrix.android.sdk.api.session.crypto.crosssigning.DeviceTrustLevel -import org.matrix.android.sdk.api.session.crypto.crosssigning.UserIdentity -import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo -import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap -import org.matrix.android.sdk.internal.crypto.model.CryptoInfoMapper -import org.matrix.android.sdk.internal.crypto.model.rest.KeysQueryResponse -import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore -import org.matrix.android.sdk.internal.crypto.store.UserDataToStore -import org.matrix.android.sdk.internal.crypto.tasks.DownloadKeysForUsersTask -import org.matrix.android.sdk.internal.session.SessionScope -import org.matrix.android.sdk.internal.session.sync.SyncTokenStore -import org.matrix.android.sdk.internal.util.logLimit -import org.matrix.android.sdk.internal.util.time.Clock -import timber.log.Timber -import javax.inject.Inject - -// Legacy name: MXDeviceList -@SessionScope -internal class DeviceListManager @Inject constructor( - private val cryptoStore: IMXCryptoStore, - private val olmDevice: MXOlmDevice, - private val syncTokenStore: SyncTokenStore, - private val credentials: Credentials, - private val downloadKeysForUsersTask: DownloadKeysForUsersTask, - private val cryptoSessionInfoProvider: CryptoSessionInfoProvider, - coroutineDispatchers: MatrixCoroutineDispatchers, - private val cryptoCoroutineScope: CoroutineScope, - private val clock: Clock, - matrixConfiguration: MatrixConfiguration -) { - - private val metricPlugins = matrixConfiguration.metricPlugins - - interface UserDevicesUpdateListener { - fun onUsersDeviceUpdate(userIds: List) - } - - private val deviceChangeListeners = mutableListOf() - - fun addListener(listener: UserDevicesUpdateListener) { - synchronized(deviceChangeListeners) { - deviceChangeListeners.add(listener) - } - } - - fun removeListener(listener: UserDevicesUpdateListener) { - synchronized(deviceChangeListeners) { - deviceChangeListeners.remove(listener) - } - } - - private fun dispatchDeviceChange(users: List) { - synchronized(deviceChangeListeners) { - deviceChangeListeners.forEach { - try { - it.onUsersDeviceUpdate(users) - } catch (failure: Throwable) { - Timber.e(failure, "Failed to dispatch device change") - } - } - } - } - - // HS not ready for retry - private val notReadyToRetryHS = mutableSetOf() - - private val cryptoCoroutineContext = coroutineDispatchers.crypto - - // Reset in progress status in case of restart - suspend fun recover() { - withContext(cryptoCoroutineContext) { - var isUpdated = false - val deviceTrackingStatuses = cryptoStore.getDeviceTrackingStatuses().toMutableMap() - for ((userId, status) in deviceTrackingStatuses) { - if (TRACKING_STATUS_DOWNLOAD_IN_PROGRESS == status || TRACKING_STATUS_UNREACHABLE_SERVER == status) { - // if a download was in progress when we got shut down, it isn't any more. - deviceTrackingStatuses[userId] = TRACKING_STATUS_PENDING_DOWNLOAD - isUpdated = true - } - } - if (isUpdated) { - cryptoStore.saveDeviceTrackingStatuses(deviceTrackingStatuses) - } - } - } - - /** - * Tells if the key downloads should be tried. - * - * @param userId the userId - * @return true if the keys download can be retrieved - */ - private fun canRetryKeysDownload(userId: String): Boolean { - var res = false - - if (':' in userId) { - try { - synchronized(notReadyToRetryHS) { - res = !notReadyToRetryHS.contains(userId.substringAfter(':')) - } - } catch (e: Exception) { - Timber.e(e, "## CRYPTO | canRetryKeysDownload() failed") - } - } - - return res - } - - /** - * Clear the unavailable server lists. - */ - private fun clearUnavailableServersList() { - synchronized(notReadyToRetryHS) { - notReadyToRetryHS.clear() - } - } - - fun onRoomMembersLoadedFor(roomId: String) { - cryptoCoroutineScope.launch(cryptoCoroutineContext) { - if (cryptoSessionInfoProvider.isRoomEncrypted(roomId)) { - // It's OK to track also device for invited users - val userIds = cryptoSessionInfoProvider.getRoomUserIds(roomId, true) - startTrackingDeviceList(userIds) - refreshOutdatedDeviceLists() - } - } - } - - /** - * Mark the cached device list for the given user outdated - * flag the given user for device-list tracking, if they are not already. - * - * @param userIds the user ids list - */ - fun startTrackingDeviceList(userIds: List) { - var isUpdated = false - val deviceTrackingStatuses = cryptoStore.getDeviceTrackingStatuses().toMutableMap() - - for (userId in userIds) { - if (!deviceTrackingStatuses.containsKey(userId) || TRACKING_STATUS_NOT_TRACKED == deviceTrackingStatuses[userId]) { - Timber.v("## CRYPTO | startTrackingDeviceList() : Now tracking device list for $userId") - deviceTrackingStatuses[userId] = TRACKING_STATUS_PENDING_DOWNLOAD - isUpdated = true - } - } - - if (isUpdated) { - cryptoStore.saveDeviceTrackingStatuses(deviceTrackingStatuses) - } - } - - /** - * Update the devices list statuses. - * - * @param changed the user ids list which have new devices - * @param left the user ids list which left a room - */ - fun handleDeviceListsChanges(changed: Collection, left: Collection) { - Timber.v("## CRYPTO: handleDeviceListsChanges changed: ${changed.logLimit()} / left: ${left.logLimit()}") - var isUpdated = false - val deviceTrackingStatuses = cryptoStore.getDeviceTrackingStatuses().toMutableMap() - - if (changed.isNotEmpty() || left.isNotEmpty()) { - clearUnavailableServersList() - } - - for (userId in changed) { - if (deviceTrackingStatuses.containsKey(userId)) { - Timber.v("## CRYPTO | handleDeviceListsChanges() : Marking device list outdated for $userId") - deviceTrackingStatuses[userId] = TRACKING_STATUS_PENDING_DOWNLOAD - isUpdated = true - } - } - - for (userId in left) { - if (deviceTrackingStatuses.containsKey(userId)) { - Timber.v("## CRYPTO | handleDeviceListsChanges() : No longer tracking device list for $userId") - deviceTrackingStatuses[userId] = TRACKING_STATUS_NOT_TRACKED - isUpdated = true - } - } - - if (isUpdated) { - cryptoStore.saveDeviceTrackingStatuses(deviceTrackingStatuses) - } - } - - /** - * This will flag each user whose devices we are tracking as in need of an update. - */ - fun invalidateAllDeviceLists() { - handleDeviceListsChanges(cryptoStore.getDeviceTrackingStatuses().keys, emptyList()) - } - - /** - * The keys download failed. - * - * @param userIds the user ids list - */ - private fun onKeysDownloadFailed(userIds: List) { - val deviceTrackingStatuses = cryptoStore.getDeviceTrackingStatuses().toMutableMap() - userIds.associateWithTo(deviceTrackingStatuses) { TRACKING_STATUS_PENDING_DOWNLOAD } - cryptoStore.saveDeviceTrackingStatuses(deviceTrackingStatuses) - } - - /** - * The keys download succeeded. - * - * @param userIds the userIds list - * @param failures the failure map. - */ - private fun onKeysDownloadSucceed(userIds: List, failures: Map>?): MXUsersDevicesMap { - if (failures != null) { - for ((k, value) in failures) { - val statusCode = when (val status = value["status"]) { - is Double -> status.toInt() - is Int -> status.toInt() - else -> 0 - } - if (statusCode == 503) { - synchronized(notReadyToRetryHS) { - notReadyToRetryHS.add(k) - } - } - } - } - val deviceTrackingStatuses = cryptoStore.getDeviceTrackingStatuses().toMutableMap() - val usersDevicesInfoMap = MXUsersDevicesMap() - for (userId in userIds) { - val devices = cryptoStore.getUserDevices(userId) - if (null == devices) { - if (canRetryKeysDownload(userId)) { - deviceTrackingStatuses[userId] = TRACKING_STATUS_PENDING_DOWNLOAD - Timber.e("failed to retry the devices of $userId : retry later") - } else { - if (deviceTrackingStatuses.containsKey(userId) && TRACKING_STATUS_DOWNLOAD_IN_PROGRESS == deviceTrackingStatuses[userId]) { - deviceTrackingStatuses[userId] = TRACKING_STATUS_UNREACHABLE_SERVER - Timber.e("failed to retry the devices of $userId : the HS is not available") - } - } - } else { - if (deviceTrackingStatuses.containsKey(userId) && TRACKING_STATUS_DOWNLOAD_IN_PROGRESS == deviceTrackingStatuses[userId]) { - // we didn't get any new invalidations since this download started: - // this user's device list is now up to date. - deviceTrackingStatuses[userId] = TRACKING_STATUS_UP_TO_DATE - Timber.v("Device list for $userId now up to date") - } - // And the response result - usersDevicesInfoMap.setObjects(userId, devices) - } - } - cryptoStore.saveDeviceTrackingStatuses(deviceTrackingStatuses) - - dispatchDeviceChange(userIds) - return usersDevicesInfoMap - } - - /** - * Download the device keys for a list of users and stores the keys in the MXStore. - * It must be called in getEncryptingThreadHandler() thread. - * - * @param userIds The users to fetch. - * @param forceDownload Always download the keys even if cached. - */ - suspend fun downloadKeys(userIds: List?, forceDownload: Boolean): MXUsersDevicesMap { - Timber.v("## CRYPTO | downloadKeys() : forceDownload $forceDownload : $userIds") - // Map from userId -> deviceId -> MXDeviceInfo - val stored = MXUsersDevicesMap() - - // List of user ids we need to download keys for - val downloadUsers = ArrayList() - if (null != userIds) { - if (forceDownload) { - downloadUsers.addAll(userIds) - } else { - for (userId in userIds) { - val status = cryptoStore.getDeviceTrackingStatus(userId, TRACKING_STATUS_NOT_TRACKED) - // downloading keys ->the keys download won't be triggered twice but the callback requires the dedicated keys - // not yet retrieved - if (TRACKING_STATUS_UP_TO_DATE != status && TRACKING_STATUS_UNREACHABLE_SERVER != status) { - downloadUsers.add(userId) - } else { - val devices = cryptoStore.getUserDevices(userId) - // should always be true - if (devices != null) { - stored.setObjects(userId, devices) - } else { - downloadUsers.add(userId) - } - } - } - } - } - return if (downloadUsers.isEmpty()) { - Timber.v("## CRYPTO | downloadKeys() : no new user device") - stored - } else { - Timber.v("## CRYPTO | downloadKeys() : starts") - val t0 = clock.epochMillis() - try { - val result = doKeyDownloadForUsers(downloadUsers) - Timber.v("## CRYPTO | downloadKeys() : doKeyDownloadForUsers succeeds after ${clock.epochMillis() - t0} ms") - result.also { - it.addEntriesFromMap(stored) - } - } catch (failure: Throwable) { - Timber.w(failure, "## CRYPTO | downloadKeys() : doKeyDownloadForUsers failed after ${clock.epochMillis() - t0} ms") - if (forceDownload) { - throw failure - } else { - stored - } - } - } - } - - /** - * Download the devices keys for a set of users. - * - * @param downloadUsers the user ids list - */ - private suspend fun doKeyDownloadForUsers(downloadUsers: List): MXUsersDevicesMap { - Timber.v("## CRYPTO | doKeyDownloadForUsers() : doKeyDownloadForUsers ${downloadUsers.logLimit()}") - // get the user ids which did not already trigger a keys download - val filteredUsers = downloadUsers.filter { MatrixPatterns.isUserId(it) } - if (filteredUsers.isEmpty()) { - // trigger nothing - return MXUsersDevicesMap() - } - val params = DownloadKeysForUsersTask.Params(filteredUsers, syncTokenStore.getLastToken()) - val relevantPlugins = metricPlugins.filterIsInstance() - - val response: KeysQueryResponse - relevantPlugins.measureMetric { - response = try { - downloadKeysForUsersTask.execute(params) - } catch (throwable: Throwable) { - Timber.e(throwable, "## CRYPTO | doKeyDownloadForUsers(): error") - if (throwable is CancellationException) { - // the crypto module is getting closed, so we cannot access the DB anymore - Timber.w("The crypto module is closed, ignoring this error") - } else { - onKeysDownloadFailed(filteredUsers) - } - throw throwable - } - Timber.v("## CRYPTO | doKeyDownloadForUsers() : Got keys for " + filteredUsers.size + " users") - } - - val userDataToStore = UserDataToStore() - - for (userId in filteredUsers) { - // al devices = - val models = response.deviceKeys?.get(userId)?.mapValues { entry -> CryptoInfoMapper.map(entry.value) } - - Timber.v("## CRYPTO | doKeyDownloadForUsers() : Got keys for $userId : $models") - if (!models.isNullOrEmpty()) { - val workingCopy = models.toMutableMap() - for ((deviceId, deviceInfo) in models) { - // Get the potential previously store device keys for this device - val previouslyStoredDeviceKeys = cryptoStore.getUserDevice(userId, deviceId) - - // in some race conditions (like unit tests) - // the self device must be seen as verified - if (deviceInfo.deviceId == credentials.deviceId && userId == credentials.userId) { - deviceInfo.trustLevel = DeviceTrustLevel(previouslyStoredDeviceKeys?.trustLevel?.crossSigningVerified ?: false, true) - } - // Validate received keys - if (!validateDeviceKeys(deviceInfo, userId, deviceId, previouslyStoredDeviceKeys)) { - // New device keys are not valid. Do not store them - workingCopy.remove(deviceId) - if (null != previouslyStoredDeviceKeys) { - // But keep old validated ones if any - workingCopy[deviceId] = previouslyStoredDeviceKeys - } - } else if (null != previouslyStoredDeviceKeys) { - // The verified status is not sync'ed with hs. - // This is a client side information, valid only for this client. - // So, transfer its previous value - workingCopy[deviceId]!!.trustLevel = previouslyStoredDeviceKeys.trustLevel - } - } - // Update the store - // Note that devices which aren't in the response will be removed from the stores - userDataToStore.userDevices[userId] = workingCopy - } - - val masterKey = response.masterKeys?.get(userId)?.toCryptoModel().also { - Timber.v("## CRYPTO | CrossSigning : Got keys for $userId : MSK ${it?.unpaddedBase64PublicKey}") - } - val selfSigningKey = response.selfSigningKeys?.get(userId)?.toCryptoModel()?.also { - Timber.v("## CRYPTO | CrossSigning : Got keys for $userId : SSK ${it.unpaddedBase64PublicKey}") - } - val userSigningKey = response.userSigningKeys?.get(userId)?.toCryptoModel()?.also { - Timber.v("## CRYPTO | CrossSigning : Got keys for $userId : USK ${it.unpaddedBase64PublicKey}") - } - userDataToStore.userIdentities[userId] = UserIdentity( - masterKey = masterKey, - selfSigningKey = selfSigningKey, - userSigningKey = userSigningKey - ) - } - - cryptoStore.storeData(userDataToStore) - - // Update devices trust for these users - // dispatchDeviceChange(downloadUsers) - - return onKeysDownloadSucceed(filteredUsers, response.failures) - } - - /** - * Validate device keys. - * This method must called on getEncryptingThreadHandler() thread. - * - * @param deviceKeys the device keys to validate. - * @param userId the id of the user of the device. - * @param deviceId the id of the device. - * @param previouslyStoredDeviceKeys the device keys we received before for this device - * @return true if succeeds - */ - private fun validateDeviceKeys(deviceKeys: CryptoDeviceInfo?, userId: String, deviceId: String, previouslyStoredDeviceKeys: CryptoDeviceInfo?): Boolean { - if (null == deviceKeys) { - Timber.e("## CRYPTO | validateDeviceKeys() : deviceKeys is null from $userId:$deviceId") - return false - } - - if (null == deviceKeys.keys) { - Timber.e("## CRYPTO | validateDeviceKeys() : deviceKeys.keys is null from $userId:$deviceId") - return false - } - - if (null == deviceKeys.signatures) { - Timber.e("## CRYPTO | validateDeviceKeys() : deviceKeys.signatures is null from $userId:$deviceId") - return false - } - - // Check that the user_id and device_id in the received deviceKeys are correct - if (deviceKeys.userId != userId) { - Timber.e("## CRYPTO | validateDeviceKeys() : Mismatched user_id ${deviceKeys.userId} from $userId:$deviceId") - return false - } - - if (deviceKeys.deviceId != deviceId) { - Timber.e("## CRYPTO | validateDeviceKeys() : Mismatched device_id ${deviceKeys.deviceId} from $userId:$deviceId") - return false - } - - val signKeyId = "ed25519:" + deviceKeys.deviceId - val signKey = deviceKeys.keys[signKeyId] - - if (null == signKey) { - Timber.e("## CRYPTO | validateDeviceKeys() : Device $userId:${deviceKeys.deviceId} has no ed25519 key") - return false - } - - val signatureMap = deviceKeys.signatures[userId] - - if (null == signatureMap) { - Timber.e("## CRYPTO | validateDeviceKeys() : Device $userId:${deviceKeys.deviceId} has no map for $userId") - return false - } - - val signature = signatureMap[signKeyId] - - if (null == signature) { - Timber.e("## CRYPTO | validateDeviceKeys() : Device $userId:${deviceKeys.deviceId} is not signed") - return false - } - - var isVerified = false - var errorMessage: String? = null - - try { - olmDevice.verifySignature(signKey, deviceKeys.signalableJSONDictionary(), signature) - isVerified = true - } catch (e: Exception) { - errorMessage = e.message - } - - if (!isVerified) { - Timber.e( - "## CRYPTO | validateDeviceKeys() : Unable to verify signature on device " + userId + ":" + - deviceKeys.deviceId + " with error " + errorMessage - ) - return false - } - - if (null != previouslyStoredDeviceKeys) { - if (previouslyStoredDeviceKeys.fingerprint() != signKey) { - // This should only happen if the list has been MITMed; we are - // best off sticking with the original keys. - // - // Should we warn the user about it somehow? - Timber.e( - "## CRYPTO | validateDeviceKeys() : WARNING:Ed25519 key for device " + userId + ":" + - deviceKeys.deviceId + " has changed : " + - previouslyStoredDeviceKeys.fingerprint() + " -> " + signKey - ) - - Timber.e("## CRYPTO | validateDeviceKeys() : $previouslyStoredDeviceKeys -> $deviceKeys") - Timber.e("## CRYPTO | validateDeviceKeys() : ${previouslyStoredDeviceKeys.keys} -> ${deviceKeys.keys}") - - return false - } - } - - return true - } - - /** - * Start device queries for any users who sent us an m.new_device recently - * This method must be called on getEncryptingThreadHandler() thread. - */ - suspend fun refreshOutdatedDeviceLists() { - Timber.v("## CRYPTO | refreshOutdatedDeviceLists()") - val deviceTrackingStatuses = cryptoStore.getDeviceTrackingStatuses().toMutableMap() - - val users = deviceTrackingStatuses.keys.filterTo(mutableListOf()) { userId -> - TRACKING_STATUS_PENDING_DOWNLOAD == deviceTrackingStatuses[userId] - } - - if (users.isEmpty()) { - return - } - - // update the statuses - users.associateWithTo(deviceTrackingStatuses) { TRACKING_STATUS_DOWNLOAD_IN_PROGRESS } - - cryptoStore.saveDeviceTrackingStatuses(deviceTrackingStatuses) - runCatching { - doKeyDownloadForUsers(users) - }.fold( - { - Timber.v("## CRYPTO | refreshOutdatedDeviceLists() : done") - }, - { - Timber.e(it, "## CRYPTO | refreshOutdatedDeviceLists() : ERROR updating device keys for users $users") - } - ) - } - - companion object { - - /** - * State transition diagram for DeviceList.deviceTrackingStatus. - *

-         *
-         *                                   |
-         *        stopTrackingDeviceList     V
-         *      +---------------------> NOT_TRACKED
-         *      |                            |
-         *      +<--------------------+      | startTrackingDeviceList
-         *      |                     |      V
-         *      |   +-------------> PENDING_DOWNLOAD <--------------------+-+
-         *      |   |                      ^ |                            | |
-         *      |   | restart     download | |  start download            | | invalidateUserDeviceList
-         *      |   | client        failed | |                            | |
-         *      |   |                      | V                            | |
-         *      |   +------------ DOWNLOAD_IN_PROGRESS -------------------+ |
-         *      |                    |       |                              |
-         *      +<-------------------+       |  download successful         |
-         *      ^                            V                              |
-         *      +----------------------- UP_TO_DATE ------------------------+
-         *
-         * 
- */ - - const val TRACKING_STATUS_NOT_TRACKED = -1 - const val TRACKING_STATUS_PENDING_DOWNLOAD = 1 - const val TRACKING_STATUS_DOWNLOAD_IN_PROGRESS = 2 - const val TRACKING_STATUS_UP_TO_DATE = 3 - const val TRACKING_STATUS_UNREACHABLE_SERVER = 4 - } -} diff --git a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/EventDecryptor.kt b/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/EventDecryptor.kt deleted file mode 100644 index c98d8e5278..0000000000 --- a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/EventDecryptor.kt +++ /dev/null @@ -1,283 +0,0 @@ -/* - * Copyright 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. - */ - -package org.matrix.android.sdk.internal.crypto - -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.launch -import kotlinx.coroutines.sync.Mutex -import kotlinx.coroutines.sync.withLock -import kotlinx.coroutines.withContext -import org.matrix.android.sdk.api.MatrixCallback -import org.matrix.android.sdk.api.MatrixCoroutineDispatchers -import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_OLM -import org.matrix.android.sdk.api.extensions.tryOrNull -import org.matrix.android.sdk.api.logger.LoggerTag -import org.matrix.android.sdk.api.session.crypto.MXCryptoError -import org.matrix.android.sdk.api.session.crypto.model.MXEventDecryptionResult -import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap -import org.matrix.android.sdk.api.session.crypto.model.OlmDecryptionResult -import org.matrix.android.sdk.api.session.events.model.Event -import org.matrix.android.sdk.api.session.events.model.EventType -import org.matrix.android.sdk.api.session.events.model.content.OlmEventContent -import org.matrix.android.sdk.api.session.events.model.toContent -import org.matrix.android.sdk.api.session.events.model.toModel -import org.matrix.android.sdk.internal.crypto.actions.EnsureOlmSessionsForDevicesAction -import org.matrix.android.sdk.internal.crypto.actions.MessageEncrypter -import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore -import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask -import org.matrix.android.sdk.internal.extensions.foldToCallback -import org.matrix.android.sdk.internal.session.SessionScope -import org.matrix.android.sdk.internal.util.time.Clock -import timber.log.Timber -import javax.inject.Inject - -private const val SEND_TO_DEVICE_RETRY_COUNT = 3 - -private val loggerTag = LoggerTag("EventDecryptor", LoggerTag.CRYPTO) - -@SessionScope -internal class EventDecryptor @Inject constructor( - private val cryptoCoroutineScope: CoroutineScope, - private val coroutineDispatchers: MatrixCoroutineDispatchers, - private val clock: Clock, - private val roomDecryptorProvider: RoomDecryptorProvider, - private val messageEncrypter: MessageEncrypter, - private val sendToDeviceTask: SendToDeviceTask, - private val deviceListManager: DeviceListManager, - private val ensureOlmSessionsForDevicesAction: EnsureOlmSessionsForDevicesAction, - private val cryptoStore: IMXCryptoStore, -) { - - /** - * Rate limit unwedge attempt, should we persist that? - */ - private val lastNewSessionForcedDates = mutableMapOf() - - data class WedgedDeviceInfo( - val userId: String, - val senderKey: String? - ) - - private val wedgedMutex = Mutex() - private val wedgedDevices = mutableListOf() - - /** - * Decrypt an event. - * - * @param event the raw event. - * @param timeline the id of the timeline where the event is decrypted. It is used to prevent replay attack. - * @return the MXEventDecryptionResult data, or throw in case of error - */ - @Throws(MXCryptoError::class) - suspend fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult { - return internalDecryptEvent(event, timeline) - } - - /** - * Decrypt an event and save the result in the given event. - * - * @param event the raw event. - * @param timeline the id of the timeline where the event is decrypted. It is used to prevent replay attack. - */ - suspend fun decryptEventAndSaveResult(event: Event, timeline: String) { - // event is not encrypted or already decrypted - if (event.getClearType() != EventType.ENCRYPTED) return - - tryOrNull(message = "decryptEventAndSaveResult | Unable to decrypt the event") { - decryptEvent(event, timeline) - } - ?.let { result -> - event.mxDecryptionResult = OlmDecryptionResult( - payload = result.clearEvent, - senderKey = result.senderCurve25519Key, - keysClaimed = result.claimedEd25519Key?.let { mapOf("ed25519" to it) }, - forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain, - verificationState = result.messageVerificationState - ) - } - } - - /** - * Decrypt an event asynchronously. - * - * @param event the raw event. - * @param timeline the id of the timeline where the event is decrypted. It is used to prevent replay attack. - * @param callback the callback to return data or null - */ - fun decryptEventAsync(event: Event, timeline: String, callback: MatrixCallback) { - // is it needed to do that on the crypto scope?? - cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { - runCatching { - internalDecryptEvent(event, timeline) - }.foldToCallback(callback) - } - } - - /** - * Decrypt an event. - * - * @param event the raw event. - * @param timeline the id of the timeline where the event is decrypted. It is used to prevent replay attack. - * @return the MXEventDecryptionResult data, or null in case of error - */ - @Throws(MXCryptoError::class) - private suspend fun internalDecryptEvent(event: Event, timeline: String): MXEventDecryptionResult { - val eventContent = event.content - if (eventContent == null) { - Timber.tag(loggerTag.value).e("decryptEvent : empty event content") - throw MXCryptoError.Base(MXCryptoError.ErrorType.BAD_ENCRYPTED_MESSAGE, MXCryptoError.BAD_ENCRYPTED_MESSAGE_REASON) - } else if (event.isRedacted()) { - // we shouldn't attempt to decrypt a redacted event because the content is cleared and decryption will fail because of null algorithm - return MXEventDecryptionResult( - clearEvent = mapOf( - "room_id" to event.roomId.orEmpty(), - "type" to EventType.MESSAGE, - "content" to emptyMap(), - "unsigned" to event.unsignedData.toContent() - ) - ) - } else { - val algorithm = eventContent["algorithm"]?.toString() - val alg = roomDecryptorProvider.getOrCreateRoomDecryptor(event.roomId, algorithm) - if (alg == null) { - val reason = String.format(MXCryptoError.UNABLE_TO_DECRYPT_REASON, event.eventId, algorithm) - Timber.tag(loggerTag.value).e("decryptEvent() : $reason") - throw MXCryptoError.Base(MXCryptoError.ErrorType.UNABLE_TO_DECRYPT, reason) - } else { - try { - return alg.decryptEvent(event, timeline) - } catch (mxCryptoError: MXCryptoError) { - Timber.tag(loggerTag.value).d("internalDecryptEvent : Failed to decrypt ${event.eventId} reason: $mxCryptoError") - if (algorithm == MXCRYPTO_ALGORITHM_OLM) { - if (mxCryptoError is MXCryptoError.Base && - mxCryptoError.errorType == MXCryptoError.ErrorType.BAD_ENCRYPTED_MESSAGE) { - // need to find sending device - val olmContent = event.content.toModel() - if (event.senderId != null && olmContent?.senderKey != null) { - markOlmSessionForUnwedging(event.senderId, olmContent.senderKey) - } else { - Timber.tag(loggerTag.value).d("Can't mark as wedge malformed") - } - } - } - throw mxCryptoError - } - } - } - } - - private suspend fun markOlmSessionForUnwedging(senderId: String, senderKey: String) { - wedgedMutex.withLock { - val info = WedgedDeviceInfo(senderId, senderKey) - if (!wedgedDevices.contains(info)) { - Timber.tag(loggerTag.value).d("Marking device from $senderId key:$senderKey as wedged") - wedgedDevices.add(info) - } - } - } - - // coroutineDispatchers.crypto scope - suspend fun unwedgeDevicesIfNeeded() { - // handle wedged devices - // Some olm decryption have failed and some device are wedged - // we should force start a new session for those - Timber.tag(loggerTag.value).v("Unwedging: ${wedgedDevices.size} are wedged") - // get the one that should be retried according to rate limit - val now = clock.epochMillis() - val toUnwedge = wedgedMutex.withLock { - wedgedDevices.filter { - val lastForcedDate = lastNewSessionForcedDates[it] ?: 0 - if (now - lastForcedDate < DefaultCryptoService.CRYPTO_MIN_FORCE_SESSION_PERIOD_MILLIS) { - Timber.tag(loggerTag.value).d("Unwedging, New session for $it already forced with device at $lastForcedDate") - return@filter false - } - // let's already mark that we tried now - lastNewSessionForcedDates[it] = now - true - } - } - - if (toUnwedge.isEmpty()) { - Timber.tag(loggerTag.value).v("Nothing to unwedge") - return - } - Timber.tag(loggerTag.value).d("Unwedging, trying to create new session for ${toUnwedge.size} devices") - - toUnwedge - .chunked(100) // safer to chunk if we ever have lots of wedged devices - .forEach { wedgedList -> - val groupedByUserId = wedgedList.groupBy { it.userId } - // lets download keys if needed - withContext(coroutineDispatchers.io) { - deviceListManager.downloadKeys(groupedByUserId.keys.toList(), false) - } - - // find the matching devices - groupedByUserId - .map { groupedByUser -> - val userId = groupedByUser.key - val wedgeSenderKeysForUser = groupedByUser.value.map { it.senderKey } - val knownDevices = cryptoStore.getUserDevices(userId)?.values.orEmpty() - userId to wedgeSenderKeysForUser.mapNotNull { senderKey -> - knownDevices.firstOrNull { it.identityKey() == senderKey } - } - } - .toMap() - .let { deviceList -> - try { - // force creating new outbound session and mark them as most recent to - // be used for next encryption (dummy) - val sessionToUse = ensureOlmSessionsForDevicesAction.handle(deviceList, true) - Timber.tag(loggerTag.value).d("Unwedging, found ${sessionToUse.map.size} to send dummy to") - - // Now send a dummy message on that session so the other side knows about it. - val payloadJson = mapOf( - "type" to EventType.DUMMY - ) - val sendToDeviceMap = MXUsersDevicesMap() - sessionToUse.map.values - .flatMap { it.values } - .map { it.deviceInfo } - .forEach { deviceInfo -> - Timber.tag(loggerTag.value).v("encrypting dummy to ${deviceInfo.deviceId}") - val encodedPayload = messageEncrypter.encryptMessage(payloadJson, listOf(deviceInfo)) - sendToDeviceMap.setObject(deviceInfo.userId, deviceInfo.deviceId, encodedPayload) - } - - // now let's send that - val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, sendToDeviceMap) - withContext(coroutineDispatchers.io) { - sendToDeviceTask.executeRetry(sendToDeviceParams, remainingRetry = SEND_TO_DEVICE_RETRY_COUNT) - } - - deviceList.values.flatten().forEach { deviceInfo -> - wedgedMutex.withLock { - wedgedDevices.removeAll { - it.senderKey == deviceInfo.identityKey() && - it.userId == deviceInfo.userId - } - } - } - } catch (failure: Throwable) { - deviceList.flatMap { it.value }.joinToString { it.shortDebugString() }.let { - Timber.tag(loggerTag.value).e(failure, "## Failed to unwedge devices: $it}") - } - } - } - } - } -} diff --git a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/InboundGroupSessionStore.kt b/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/InboundGroupSessionStore.kt deleted file mode 100644 index 6d197a09ed..0000000000 --- a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/InboundGroupSessionStore.kt +++ /dev/null @@ -1,126 +0,0 @@ -/* - * Copyright 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. - */ - -package org.matrix.android.sdk.internal.crypto - -import android.util.LruCache -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.launch -import kotlinx.coroutines.sync.Mutex -import org.matrix.android.sdk.api.MatrixCoroutineDispatchers -import org.matrix.android.sdk.api.logger.LoggerTag -import org.matrix.android.sdk.internal.crypto.model.MXInboundMegolmSessionWrapper -import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore -import timber.log.Timber -import javax.inject.Inject - -internal data class InboundGroupSessionHolder( - val wrapper: MXInboundMegolmSessionWrapper, - val mutex: Mutex = Mutex() -) - -private val loggerTag = LoggerTag("InboundGroupSessionStore", LoggerTag.CRYPTO) - -/** - * Allows to cache and batch store operations on inbound group session store. - * Because it is used in the decrypt flow, that can be called quite rapidly - */ -internal class InboundGroupSessionStore @Inject constructor( - private val store: IMXCryptoStore, - private val cryptoCoroutineScope: CoroutineScope, - private val coroutineDispatchers: MatrixCoroutineDispatchers -) { - - private data class CacheKey( - val sessionId: String, - val senderKey: String - ) - - private val sessionCache = object : LruCache(100) { - override fun entryRemoved(evicted: Boolean, key: CacheKey?, oldValue: InboundGroupSessionHolder?, newValue: InboundGroupSessionHolder?) { - if (oldValue != null) { - cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { - Timber.tag(loggerTag.value).v("## Inbound: entryRemoved ${oldValue.wrapper.roomId}-${oldValue.wrapper.senderKey}") - // store.storeInboundGroupSessions(listOf(oldValue).map { it.wrapper }) - oldValue.wrapper.session.releaseSession() - } - } - } - } - - @Synchronized - fun clear() { - sessionCache.evictAll() - } - - @Synchronized - fun getInboundGroupSession(sessionId: String, senderKey: String): InboundGroupSessionHolder? { - val known = sessionCache[CacheKey(sessionId, senderKey)] - Timber.tag(loggerTag.value).v("## Inbound: getInboundGroupSession $sessionId in cache ${known != null}") - return known - ?: store.getInboundGroupSession(sessionId, senderKey)?.also { - Timber.tag(loggerTag.value).v("## Inbound: getInboundGroupSession cache populate ${it.roomId}") - sessionCache.put(CacheKey(sessionId, senderKey), InboundGroupSessionHolder(it)) - }?.let { - InboundGroupSessionHolder(it) - } - } - - @Synchronized - fun replaceGroupSession(old: InboundGroupSessionHolder, new: InboundGroupSessionHolder, sessionId: String, senderKey: String) { - Timber.tag(loggerTag.value).v("## Replacing outdated session ${old.wrapper.roomId}-${old.wrapper.senderKey}") - store.removeInboundGroupSession(sessionId, senderKey) - sessionCache.remove(CacheKey(sessionId, senderKey)) - - // release removed session - old.wrapper.session.releaseSession() - - internalStoreGroupSession(new, sessionId, senderKey) - } - - @Synchronized - fun updateToSafe(old: InboundGroupSessionHolder, sessionId: String, senderKey: String) { - Timber.tag(loggerTag.value).v("## updateToSafe for session ${old.wrapper.roomId}-${old.wrapper.senderKey}") - - store.storeInboundGroupSessions( - listOf( - old.wrapper.copy( - sessionData = old.wrapper.sessionData.copy(trusted = true) - ) - ) - ) - // will release it :/ - sessionCache.remove(CacheKey(sessionId, senderKey)) - } - - @Synchronized - fun storeInBoundGroupSession(holder: InboundGroupSessionHolder, sessionId: String, senderKey: String) { - internalStoreGroupSession(holder, sessionId, senderKey) - } - - private fun internalStoreGroupSession(holder: InboundGroupSessionHolder, sessionId: String, senderKey: String) { - Timber.tag(loggerTag.value).v("## Inbound: getInboundGroupSession mark as dirty ${holder.wrapper.roomId}-${holder.wrapper.senderKey}") - - if (sessionCache[CacheKey(sessionId, senderKey)] == null) { - // first time seen, put it in memory cache while waiting for batch insert - // If it's already known, no need to update cache it's already there - sessionCache.put(CacheKey(sessionId, senderKey), holder) - } - cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { - store.storeInboundGroupSessions(listOf(holder.wrapper)) - } - } -} diff --git a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/IncomingKeyRequestManager.kt b/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/IncomingKeyRequestManager.kt deleted file mode 100644 index 729b4481e4..0000000000 --- a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/IncomingKeyRequestManager.kt +++ /dev/null @@ -1,465 +0,0 @@ -/* - * Copyright 2022 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. - */ - -package org.matrix.android.sdk.internal.crypto - -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.SupervisorJob -import kotlinx.coroutines.asCoroutineDispatcher -import kotlinx.coroutines.cancel -import kotlinx.coroutines.launch -import kotlinx.coroutines.sync.withLock -import kotlinx.coroutines.withContext -import org.matrix.android.sdk.api.MatrixCoroutineDispatchers -import org.matrix.android.sdk.api.auth.data.Credentials -import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_MEGOLM -import org.matrix.android.sdk.api.crypto.MXCryptoConfig -import org.matrix.android.sdk.api.extensions.tryOrNull -import org.matrix.android.sdk.api.logger.LoggerTag -import org.matrix.android.sdk.api.session.crypto.keyshare.GossipingRequestListener -import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo -import org.matrix.android.sdk.api.session.crypto.model.IncomingRoomKeyRequest -import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap -import org.matrix.android.sdk.api.session.crypto.model.RoomKeyRequestBody -import org.matrix.android.sdk.api.session.crypto.model.RoomKeyShareRequest -import org.matrix.android.sdk.api.session.events.model.EventType -import org.matrix.android.sdk.api.session.events.model.content.RoomKeyWithHeldContent -import org.matrix.android.sdk.api.session.events.model.content.WithHeldCode -import org.matrix.android.sdk.internal.crypto.actions.EnsureOlmSessionsForDevicesAction -import org.matrix.android.sdk.internal.crypto.actions.MessageEncrypter -import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore -import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask -import org.matrix.android.sdk.internal.session.SessionScope -import org.matrix.android.sdk.internal.task.SemaphoreCoroutineSequencer -import org.matrix.android.sdk.internal.util.time.Clock -import timber.log.Timber -import java.util.concurrent.Executors -import javax.inject.Inject -import kotlin.system.measureTimeMillis - -private val loggerTag = LoggerTag("IncomingKeyRequestManager", LoggerTag.CRYPTO) - -@SessionScope -internal class IncomingKeyRequestManager @Inject constructor( - private val credentials: Credentials, - private val cryptoStore: IMXCryptoStore, - private val ensureOlmSessionsForDevicesAction: EnsureOlmSessionsForDevicesAction, - private val olmDevice: MXOlmDevice, - private val cryptoConfig: MXCryptoConfig, - private val messageEncrypter: MessageEncrypter, - private val coroutineDispatchers: MatrixCoroutineDispatchers, - private val sendToDeviceTask: SendToDeviceTask, - private val clock: Clock, -) { - - private val dispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher() - private val outgoingRequestScope = CoroutineScope(SupervisorJob() + dispatcher) - val sequencer = SemaphoreCoroutineSequencer() - - private val incomingRequestBuffer = mutableListOf() - - // the listeners - private val gossipingRequestListeners: MutableSet = HashSet() - - enum class MegolmRequestAction { - Request, Cancel - } - - data class ValidMegolmRequestBody( - val requestId: String, - val requestingUserId: String, - val requestingDeviceId: String, - val roomId: String, - val senderKey: String, - val sessionId: String, - val action: MegolmRequestAction - ) { - fun shortDbgString() = "Request from $requestingUserId|$requestingDeviceId for session $sessionId in room $roomId" - } - - private fun RoomKeyShareRequest.toValidMegolmRequest(senderId: String): ValidMegolmRequestBody? { - val deviceId = requestingDeviceId ?: return null - val body = body ?: return null - val roomId = body.roomId ?: return null - val sessionId = body.sessionId ?: return null - val senderKey = body.senderKey ?: return null - val requestId = this.requestId ?: return null - if (body.algorithm != MXCRYPTO_ALGORITHM_MEGOLM) return null - val action = when (this.action) { - "request" -> MegolmRequestAction.Request - "request_cancellation" -> MegolmRequestAction.Cancel - else -> null - } ?: return null - return ValidMegolmRequestBody( - requestId = requestId, - requestingUserId = senderId, - requestingDeviceId = deviceId, - roomId = roomId, - senderKey = senderKey, - sessionId = sessionId, - action = action - ) - } - - fun addNewIncomingRequest(senderId: String, request: RoomKeyShareRequest) { - if (!cryptoStore.isKeyGossipingEnabled()) { - Timber.tag(loggerTag.value) - .i("Ignore incoming key request as per crypto config in room ${request.body?.roomId}") - return - } - outgoingRequestScope.launch { - // It is important to handle requests in order - sequencer.post { - val validMegolmRequest = request.toValidMegolmRequest(senderId) ?: return@post Unit.also { - Timber.tag(loggerTag.value).w("Received key request for unknown algorithm ${request.body?.algorithm}") - } - - // is there already one like that? - val existing = incomingRequestBuffer.firstOrNull { it == validMegolmRequest } - if (existing == null) { - when (validMegolmRequest.action) { - MegolmRequestAction.Request -> { - // just add to the buffer - incomingRequestBuffer.add(validMegolmRequest) - } - MegolmRequestAction.Cancel -> { - // ignore, we can't cancel as it's not known (probably already processed) - // still notify app layer if it was passed up previously - IncomingRoomKeyRequest.fromRestRequest(senderId, request, clock)?.let { iReq -> - outgoingRequestScope.launch(coroutineDispatchers.computation) { - val listenersCopy = synchronized(gossipingRequestListeners) { - gossipingRequestListeners.toList() - } - listenersCopy.onEach { - tryOrNull { - withContext(coroutineDispatchers.main) { - it.onRequestCancelled(iReq) - } - } - } - } - } - } - } - } else { - when (validMegolmRequest.action) { - MegolmRequestAction.Request -> { - // it's already in buffer, nop keep existing - } - MegolmRequestAction.Cancel -> { - // discard the request in buffer - incomingRequestBuffer.remove(existing) - outgoingRequestScope.launch(coroutineDispatchers.computation) { - val listenersCopy = synchronized(gossipingRequestListeners) { - gossipingRequestListeners.toList() - } - listenersCopy.onEach { - IncomingRoomKeyRequest.fromRestRequest(senderId, request, clock)?.let { iReq -> - withContext(coroutineDispatchers.main) { - tryOrNull { it.onRequestCancelled(iReq) } - } - } - } - } - } - } - } - } - } - } - - fun processIncomingRequests() { - outgoingRequestScope.launch { - sequencer.post { - measureTimeMillis { - Timber.tag(loggerTag.value).v("processIncomingKeyRequests : ${incomingRequestBuffer.size} request to process") - incomingRequestBuffer.forEach { - // should not happen, we only store requests - if (it.action != MegolmRequestAction.Request) return@forEach - try { - handleIncomingRequest(it) - } catch (failure: Throwable) { - // ignore and continue, should not happen - Timber.tag(loggerTag.value).w(failure, "processIncomingKeyRequests : failed to process request $it") - } - } - incomingRequestBuffer.clear() - }.let { duration -> - Timber.tag(loggerTag.value).v("Finish processing incoming key request in $duration ms") - } - } - } - } - - private suspend fun handleIncomingRequest(request: ValidMegolmRequestBody) { - // We don't want to download keys, if we don't know the device yet we won't share any how? - val requestingDevice = - cryptoStore.getUserDevice(request.requestingUserId, request.requestingDeviceId) - ?: return Unit.also { - Timber.tag(loggerTag.value).d("Ignoring key request: ${request.shortDbgString()}") - } - - cryptoStore.saveIncomingKeyRequestAuditTrail( - request.requestId, - request.roomId, - request.sessionId, - request.senderKey, - MXCRYPTO_ALGORITHM_MEGOLM, - request.requestingUserId, - request.requestingDeviceId - ) - - val roomAlgorithm = // withContext(coroutineDispatchers.crypto) { - cryptoStore.getRoomAlgorithm(request.roomId) -// } - if (roomAlgorithm != MXCRYPTO_ALGORITHM_MEGOLM) { - // strange we received a request for a room that is not encrypted - // maybe a broken state? - Timber.tag(loggerTag.value).w("Received a key request in a room with unsupported alg:$roomAlgorithm , req:${request.shortDbgString()}") - return - } - - // Is it for one of our sessions? - if (request.requestingUserId == credentials.userId) { - Timber.tag(loggerTag.value).v("handling request from own user: megolm session ${request.sessionId}") - - if (request.requestingDeviceId == credentials.deviceId) { - // ignore it's a remote echo - return - } - // If it's verified we share from the early index we know - // if not we check if it was originaly shared or not - if (requestingDevice.isVerified) { - // we share from the earliest known chain index - shareMegolmKey(request, requestingDevice, null) - } else { - shareIfItWasPreviouslyShared(request, requestingDevice) - } - } else { - if (cryptoConfig.limitRoomKeyRequestsToMyDevices) { - Timber.tag(loggerTag.value).v("Ignore request from other user as per crypto config: ${request.shortDbgString()}") - return - } - Timber.tag(loggerTag.value).v("handling request from other user: megolm session ${request.sessionId}") - if (requestingDevice.isBlocked) { - // it's blocked, so send a withheld code - sendWithheldForRequest(request, WithHeldCode.BLACKLISTED) - } else { - shareIfItWasPreviouslyShared(request, requestingDevice) - } - } - } - - private suspend fun shareIfItWasPreviouslyShared(request: ValidMegolmRequestBody, requestingDevice: CryptoDeviceInfo) { - // we don't reshare unless it was previously shared with - val wasSessionSharedWithUser = withContext(coroutineDispatchers.crypto) { - cryptoStore.getSharedSessionInfo(request.roomId, request.sessionId, requestingDevice) - } - if (wasSessionSharedWithUser.found && wasSessionSharedWithUser.chainIndex != null) { - // we share from the index it was previously shared with - shareMegolmKey(request, requestingDevice, wasSessionSharedWithUser.chainIndex.toLong()) - } else { - val isOwnDevice = requestingDevice.userId == credentials.userId - sendWithheldForRequest(request, if (isOwnDevice) WithHeldCode.UNVERIFIED else WithHeldCode.UNAUTHORISED) - // if it's our device we could delegate to the app layer to decide - if (isOwnDevice) { - outgoingRequestScope.launch(coroutineDispatchers.computation) { - val listenersCopy = synchronized(gossipingRequestListeners) { - gossipingRequestListeners.toList() - } - val iReq = IncomingRoomKeyRequest( - userId = requestingDevice.userId, - deviceId = requestingDevice.deviceId, - requestId = request.requestId, - requestBody = RoomKeyRequestBody( - algorithm = MXCRYPTO_ALGORITHM_MEGOLM, - senderKey = request.senderKey, - sessionId = request.sessionId, - roomId = request.roomId - ), - localCreationTimestamp = clock.epochMillis() - ) - listenersCopy.onEach { - withContext(coroutineDispatchers.main) { - tryOrNull { it.onRoomKeyRequest(iReq) } - } - } - } - } - } - } - - private suspend fun sendWithheldForRequest(request: ValidMegolmRequestBody, code: WithHeldCode) { - Timber.tag(loggerTag.value) - .w("Send withheld $code for req: ${request.shortDbgString()}") - val withHeldContent = RoomKeyWithHeldContent( - roomId = request.roomId, - senderKey = request.senderKey, - algorithm = MXCRYPTO_ALGORITHM_MEGOLM, - sessionId = request.sessionId, - codeString = code.value, - fromDevice = credentials.deviceId - ) - - val params = SendToDeviceTask.Params( - EventType.ROOM_KEY_WITHHELD.stable, - MXUsersDevicesMap().apply { - setObject(request.requestingUserId, request.requestingDeviceId, withHeldContent) - } - ) - try { - withContext(coroutineDispatchers.io) { - sendToDeviceTask.execute(params) - Timber.tag(loggerTag.value) - .d("Send withheld $code req: ${request.shortDbgString()}") - } - - cryptoStore.saveWithheldAuditTrail( - roomId = request.roomId, - sessionId = request.sessionId, - senderKey = request.senderKey, - algorithm = MXCRYPTO_ALGORITHM_MEGOLM, - code = code, - userId = request.requestingUserId, - deviceId = request.requestingDeviceId - ) - } catch (failure: Throwable) { - // Ignore it's not that important? - // do we want to fallback to a worker? - Timber.tag(loggerTag.value) - .w("Failed to send withheld $code req: ${request.shortDbgString()} reason:${failure.localizedMessage}") - } - } - - suspend fun manuallyAcceptRoomKeyRequest(request: IncomingRoomKeyRequest) { - request.requestId ?: return - request.deviceId ?: return - request.userId ?: return - request.requestBody?.roomId ?: return - request.requestBody.senderKey ?: return - request.requestBody.sessionId ?: return - val validReq = ValidMegolmRequestBody( - requestId = request.requestId, - requestingDeviceId = request.deviceId, - requestingUserId = request.userId, - roomId = request.requestBody.roomId, - senderKey = request.requestBody.senderKey, - sessionId = request.requestBody.sessionId, - action = MegolmRequestAction.Request - ) - val requestingDevice = - cryptoStore.getUserDevice(request.userId, request.deviceId) - ?: return Unit.also { - Timber.tag(loggerTag.value).d("Ignoring key request: ${validReq.shortDbgString()}") - } - - shareMegolmKey(validReq, requestingDevice, null) - } - - private suspend fun shareMegolmKey( - validRequest: ValidMegolmRequestBody, - requestingDevice: CryptoDeviceInfo, - chainIndex: Long? - ): Boolean { - Timber.tag(loggerTag.value) - .d("try to re-share Megolm Key at index $chainIndex for ${validRequest.shortDbgString()}") - - val devicesByUser = mapOf(validRequest.requestingUserId to listOf(requestingDevice)) - val usersDeviceMap = try { - ensureOlmSessionsForDevicesAction.handle(devicesByUser) - } catch (failure: Throwable) { - Timber.tag(loggerTag.value) - .w("Failed to establish olm session") - sendWithheldForRequest(validRequest, WithHeldCode.NO_OLM) - return false - } - - val olmSessionResult = usersDeviceMap.getObject(requestingDevice.userId, requestingDevice.deviceId) - if (olmSessionResult?.sessionId == null) { - Timber.tag(loggerTag.value) - .w("reshareKey: no session with this device, probably because there were no one-time keys") - sendWithheldForRequest(validRequest, WithHeldCode.NO_OLM) - return false - } - val sessionHolder = try { - olmDevice.getInboundGroupSession(validRequest.sessionId, validRequest.senderKey, validRequest.roomId) - } catch (failure: Throwable) { - Timber.tag(loggerTag.value) - .e(failure, "shareKeysWithDevice: failed to get session ${validRequest.requestingUserId}") - // It's unavailable - sendWithheldForRequest(validRequest, WithHeldCode.UNAVAILABLE) - return false - } - - val export = sessionHolder.mutex.withLock { - sessionHolder.wrapper.exportKeys(chainIndex) - } ?: return false.also { - Timber.tag(loggerTag.value) - .e("shareKeysWithDevice: failed to export group session ${validRequest.sessionId}") - } - - val payloadJson = mapOf( - "type" to EventType.FORWARDED_ROOM_KEY, - "content" to export - ) - - val encodedPayload = messageEncrypter.encryptMessage(payloadJson, listOf(requestingDevice)) - val sendToDeviceMap = MXUsersDevicesMap() - sendToDeviceMap.setObject(requestingDevice.userId, requestingDevice.deviceId, encodedPayload) - Timber.tag(loggerTag.value).d("reshareKey() : try sending session ${validRequest.sessionId} to ${requestingDevice.shortDebugString()}") - val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, sendToDeviceMap) - return try { - sendToDeviceTask.execute(sendToDeviceParams) - Timber.tag(loggerTag.value) - .i("successfully re-shared session ${validRequest.sessionId} to ${requestingDevice.shortDebugString()}") - cryptoStore.saveForwardKeyAuditTrail( - validRequest.roomId, - validRequest.sessionId, - validRequest.senderKey, - MXCRYPTO_ALGORITHM_MEGOLM, - requestingDevice.userId, - requestingDevice.deviceId, - chainIndex - ) - true - } catch (failure: Throwable) { - Timber.tag(loggerTag.value) - .e(failure, "fail to re-share session ${validRequest.sessionId} to ${requestingDevice.shortDebugString()}") - false - } - } - - fun addRoomKeysRequestListener(listener: GossipingRequestListener) { - synchronized(gossipingRequestListeners) { - gossipingRequestListeners.add(listener) - } - } - - fun removeRoomKeysRequestListener(listener: GossipingRequestListener) { - synchronized(gossipingRequestListeners) { - gossipingRequestListeners.remove(listener) - } - } - - fun close() { - try { - outgoingRequestScope.cancel("User Terminate") - incomingRequestBuffer.clear() - } catch (failure: Throwable) { - Timber.tag(loggerTag.value).w("Failed to shutDown request manager") - } - } -} diff --git a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/MXOlmDevice.kt b/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/MXOlmDevice.kt deleted file mode 100755 index 7b03c1d16c..0000000000 --- a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/MXOlmDevice.kt +++ /dev/null @@ -1,1014 +0,0 @@ -/* - * Copyright 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. - */ - -package org.matrix.android.sdk.internal.crypto - -import androidx.annotation.VisibleForTesting -import kotlinx.coroutines.sync.Mutex -import kotlinx.coroutines.sync.withLock -import org.matrix.android.sdk.api.extensions.orFalse -import org.matrix.android.sdk.api.extensions.tryOrNull -import org.matrix.android.sdk.api.logger.LoggerTag -import org.matrix.android.sdk.api.session.crypto.MXCryptoError -import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo -import org.matrix.android.sdk.api.session.crypto.model.MessageVerificationState -import org.matrix.android.sdk.api.session.crypto.model.OlmDecryptionResult -import org.matrix.android.sdk.api.util.JSON_DICT_PARAMETERIZED_TYPE -import org.matrix.android.sdk.api.util.JsonDict -import org.matrix.android.sdk.internal.crypto.algorithms.megolm.MXOutboundSessionInfo -import org.matrix.android.sdk.internal.crypto.algorithms.megolm.SharedWithHelper -import org.matrix.android.sdk.internal.crypto.crosssigning.CrossSigningOlm -import org.matrix.android.sdk.internal.crypto.crosssigning.canonicalSignable -import org.matrix.android.sdk.internal.crypto.model.InboundGroupSessionData -import org.matrix.android.sdk.internal.crypto.model.MXInboundMegolmSessionWrapper -import org.matrix.android.sdk.internal.crypto.model.OlmSessionWrapper -import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore -import org.matrix.android.sdk.internal.di.MoshiProvider -import org.matrix.android.sdk.internal.session.SessionScope -import org.matrix.android.sdk.internal.util.JsonCanonicalizer -import org.matrix.android.sdk.internal.util.convertFromUTF8 -import org.matrix.android.sdk.internal.util.convertToUTF8 -import org.matrix.android.sdk.internal.util.time.Clock -import org.matrix.olm.OlmAccount -import org.matrix.olm.OlmException -import org.matrix.olm.OlmInboundGroupSession -import org.matrix.olm.OlmMessage -import org.matrix.olm.OlmOutboundGroupSession -import org.matrix.olm.OlmSession -import org.matrix.olm.OlmUtility -import timber.log.Timber -import javax.inject.Inject - -private val loggerTag = LoggerTag("MXOlmDevice", LoggerTag.CRYPTO) - -// The libolm wrapper. -@SessionScope -internal class MXOlmDevice @Inject constructor( - /** - * The store where crypto data is saved. - */ - private val store: IMXCryptoStore, - private val olmSessionStore: OlmSessionStore, - private val inboundGroupSessionStore: InboundGroupSessionStore, - private val crossSigningOlm: CrossSigningOlm, - private val clock: Clock, -) { - - val mutex = Mutex() - - /** - * @return the Curve25519 key for the account. - */ - var deviceCurve25519Key: String? = null - private set - - /** - * @return the Ed25519 key for the account. - */ - var deviceEd25519Key: String? = null - private set - - // The OLM lib utility instance. - private var olmUtility: OlmUtility? = null - - private data class GroupSessionCacheItem( - val groupId: String, - val groupSession: OlmOutboundGroupSession - ) - - // The outbound group session. - // Caches active outbound session to avoid to sync with DB before read - // The key is the session id, the value the . - private val outboundGroupSessionCache: MutableMap = HashMap() - - // Store a set of decrypted message indexes for each group session. - // This partially mitigates a replay attack where a MITM resends a group - // message into the room. - // - // The Matrix SDK exposes events through MXEventTimelines. A developer can open several - // timelines from a same room so that a message can be decrypted several times but from - // a different timeline. - // So, store these message indexes per timeline id. - // - // The first level keys are timeline ids. - // The second level values is a Map that represents: - // "|||" --> eventId - private val inboundGroupSessionMessageIndexes: MutableMap> = HashMap() - - init { - // Retrieve the account from the store - try { - store.getOrCreateOlmAccount() - } catch (e: Exception) { - Timber.tag(loggerTag.value).e(e, "MXOlmDevice : cannot initialize olmAccount") - } - - try { - olmUtility = OlmUtility() - } catch (e: Exception) { - Timber.tag(loggerTag.value).e(e, "## MXOlmDevice : OlmUtility failed with error") - olmUtility = null - } - - try { - deviceCurve25519Key = store.doWithOlmAccount { it.identityKeys()[OlmAccount.JSON_KEY_IDENTITY_KEY] } - } catch (e: Exception) { - Timber.tag(loggerTag.value).e(e, "## MXOlmDevice : cannot find ${OlmAccount.JSON_KEY_IDENTITY_KEY} with error") - } - - try { - deviceEd25519Key = store.doWithOlmAccount { it.identityKeys()[OlmAccount.JSON_KEY_FINGER_PRINT_KEY] } - } catch (e: Exception) { - Timber.tag(loggerTag.value).e(e, "## MXOlmDevice : cannot find ${OlmAccount.JSON_KEY_FINGER_PRINT_KEY} with error") - } - } - - /** - * @return The current (unused, unpublished) one-time keys for this account. - */ - fun getOneTimeKeys(): Map>? { - try { - return store.doWithOlmAccount { it.oneTimeKeys() } - } catch (e: Exception) { - Timber.tag(loggerTag.value).e(e, "## getOneTimeKeys() : failed") - } - - return null - } - - /** - * @return The maximum number of one-time keys the olm account can store. - */ - fun getMaxNumberOfOneTimeKeys(): Long { - return store.doWithOlmAccount { it.maxOneTimeKeys() } - } - - /** - * Returns an unpublished fallback key. - * A call to markKeysAsPublished will mark it as published and this - * call will return null (until a call to generateFallbackKey is made). - */ - fun getFallbackKey(): MutableMap>? { - try { - return store.doWithOlmAccount { it.fallbackKey() } - } catch (e: Exception) { - Timber.tag(loggerTag.value).e("## getFallbackKey() : failed") - } - return null - } - - /** - * Generates a new fallback key if there is not already - * an unpublished one. - * @return true if a new key was generated - */ - fun generateFallbackKeyIfNeeded(): Boolean { - try { - if (!hasUnpublishedFallbackKey()) { - store.doWithOlmAccount { - it.generateFallbackKey() - store.saveOlmAccount() - } - return true - } - } catch (e: Exception) { - Timber.tag(loggerTag.value).e("## generateFallbackKey() : failed") - } - return false - } - - internal fun hasUnpublishedFallbackKey(): Boolean { - return getFallbackKey()?.get(OlmAccount.JSON_KEY_ONE_TIME_KEY).orEmpty().isNotEmpty() - } - - fun forgetFallbackKey() { - try { - store.doWithOlmAccount { - it.forgetFallbackKey() - store.saveOlmAccount() - } - } catch (e: Exception) { - Timber.tag(loggerTag.value).e("## forgetFallbackKey() : failed") - } - } - - /** - * Release the instance. - */ - fun release() { - olmUtility?.releaseUtility() - outboundGroupSessionCache.values.forEach { - it.groupSession.releaseSession() - } - outboundGroupSessionCache.clear() - inboundGroupSessionStore.clear() - olmSessionStore.clear() - } - - /** - * Signs a message with the ed25519 key for this account. - * - * @param message the message to be signed. - * @return the base64-encoded signature. - */ - fun signMessage(message: String): String? { - try { - return store.doWithOlmAccount { it.signMessage(message) } - } catch (e: Exception) { - Timber.tag(loggerTag.value).e(e, "## signMessage() : failed") - } - - return null - } - - /** - * Marks all of the one-time keys as published. - */ - fun markKeysAsPublished() { - try { - store.doWithOlmAccount { - it.markOneTimeKeysAsPublished() - store.saveOlmAccount() - } - } catch (e: Exception) { - Timber.tag(loggerTag.value).e(e, "## markKeysAsPublished() : failed") - } - } - - /** - * Generate some new one-time keys. - * - * @param numKeys number of keys to generate - */ - fun generateOneTimeKeys(numKeys: Int) { - try { - store.doWithOlmAccount { - it.generateOneTimeKeys(numKeys) - store.saveOlmAccount() - } - } catch (e: Exception) { - Timber.tag(loggerTag.value).e(e, "## generateOneTimeKeys() : failed") - } - } - - /** - * Generate a new outbound session. - * The new session will be stored in the MXStore. - * - * @param theirIdentityKey the remote user's Curve25519 identity key - * @param theirOneTimeKey the remote user's one-time Curve25519 key - * @return the session id for the outbound session. - */ - fun createOutboundSession(theirIdentityKey: String, theirOneTimeKey: String): String? { - Timber.tag(loggerTag.value).d("## createOutboundSession() ; theirIdentityKey $theirIdentityKey theirOneTimeKey $theirOneTimeKey") - var olmSession: OlmSession? = null - - try { - olmSession = OlmSession() - store.doWithOlmAccount { olmAccount -> - olmSession.initOutboundSession(olmAccount, theirIdentityKey, theirOneTimeKey) - } - - val olmSessionWrapper = OlmSessionWrapper(olmSession, 0) - - // Pretend we've received a message at this point, otherwise - // if we try to send a message to the device, it won't use - // this session - olmSessionWrapper.onMessageReceived(clock.epochMillis()) - - olmSessionStore.storeSession(olmSessionWrapper, theirIdentityKey) - - val sessionIdentifier = olmSession.sessionIdentifier() - - Timber.tag(loggerTag.value).v("## createOutboundSession() ; olmSession.sessionIdentifier: $sessionIdentifier") - return sessionIdentifier - } catch (e: Exception) { - Timber.tag(loggerTag.value).e(e, "## createOutboundSession() failed") - - olmSession?.releaseSession() - } - - return null - } - - /** - * Generate a new inbound session, given an incoming message. - * - * @param theirDeviceIdentityKey the remote user's Curve25519 identity key. - * @param messageType the message_type field from the received message (must be 0). - * @param ciphertext base64-encoded body from the received message. - * @return {{payload: string, session_id: string}} decrypted payload, and session id of new session. - */ - fun createInboundSession(theirDeviceIdentityKey: String, messageType: Int, ciphertext: String): Map? { - Timber.tag(loggerTag.value).d("## createInboundSession() : theirIdentityKey: $theirDeviceIdentityKey") - - var olmSession: OlmSession? = null - - try { - try { - olmSession = OlmSession() - store.doWithOlmAccount { olmAccount -> - olmSession.initInboundSessionFrom(olmAccount, theirDeviceIdentityKey, ciphertext) - } - } catch (e: Exception) { - Timber.tag(loggerTag.value).e(e, "## createInboundSession() : the session creation failed") - return null - } - - Timber.tag(loggerTag.value).v("## createInboundSession() : sessionId: ${olmSession.sessionIdentifier()}") - - try { - store.doWithOlmAccount { olmAccount -> - olmAccount.removeOneTimeKeys(olmSession) - store.saveOlmAccount() - } - } catch (e: Exception) { - Timber.tag(loggerTag.value).e(e, "## createInboundSession() : removeOneTimeKeys failed") - } - - val olmMessage = OlmMessage() - olmMessage.mCipherText = ciphertext - olmMessage.mType = messageType.toLong() - - var payloadString: String? = null - - try { - payloadString = olmSession.decryptMessage(olmMessage) - - val olmSessionWrapper = OlmSessionWrapper(olmSession, 0) - // This counts as a received message: set last received message time to now - olmSessionWrapper.onMessageReceived(clock.epochMillis()) - - olmSessionStore.storeSession(olmSessionWrapper, theirDeviceIdentityKey) - } catch (e: Exception) { - Timber.tag(loggerTag.value).e(e, "## createInboundSession() : decryptMessage failed") - } - - val res = HashMap() - - if (!payloadString.isNullOrEmpty()) { - res["payload"] = payloadString - } - - val sessionIdentifier = olmSession.sessionIdentifier() - - if (!sessionIdentifier.isNullOrEmpty()) { - res["session_id"] = sessionIdentifier - } - - return res - } catch (e: Exception) { - Timber.tag(loggerTag.value).e(e, "## createInboundSession() : OlmSession creation failed") - - olmSession?.releaseSession() - } - - return null - } - - /** - * Get a list of known session IDs for the given device. - * - * @param theirDeviceIdentityKey the Curve25519 identity key for the remote device. - * @return a list of known session ids for the device. - */ - fun getSessionIds(theirDeviceIdentityKey: String): List { - return olmSessionStore.getDeviceSessionIds(theirDeviceIdentityKey) - } - - /** - * Get the right olm session id for encrypting messages to the given identity key. - * - * @param theirDeviceIdentityKey the Curve25519 identity key for the remote device. - * @return the session id, or null if no established session. - */ - fun getSessionId(theirDeviceIdentityKey: String): String? { - return olmSessionStore.getLastUsedSessionId(theirDeviceIdentityKey) - } - - /** - * Encrypt an outgoing message using an existing session. - * - * @param theirDeviceIdentityKey the Curve25519 identity key for the remote device. - * @param sessionId the id of the active session - * @param payloadString the payload to be encrypted and sent - * @return the cipher text - */ - suspend fun encryptMessage(theirDeviceIdentityKey: String, sessionId: String, payloadString: String): Map? { - val olmSessionWrapper = getSessionForDevice(theirDeviceIdentityKey, sessionId) - - if (olmSessionWrapper != null) { - try { - Timber.tag(loggerTag.value).v("## encryptMessage() : olmSession.sessionIdentifier: $sessionId") - - val olmMessage = olmSessionWrapper.mutex.withLock { - olmSessionWrapper.olmSession.encryptMessage(payloadString) - } - return mapOf( - "body" to olmMessage.mCipherText, - "type" to olmMessage.mType, - ).also { - olmSessionStore.storeSession(olmSessionWrapper, theirDeviceIdentityKey) - } - } catch (e: Throwable) { - Timber.tag(loggerTag.value).e(e, "## encryptMessage() : failed to encrypt olm with device|session:$theirDeviceIdentityKey|$sessionId") - return null - } - } else { - Timber.tag(loggerTag.value).e("## encryptMessage() : Failed to encrypt unknown session $sessionId") - return null - } - } - - /** - * Decrypt an incoming message using an existing session. - * - * @param ciphertext the base64-encoded body from the received message. - * @param messageType message_type field from the received message. - * @param sessionId the id of the active session. - * @param theirDeviceIdentityKey the Curve25519 identity key for the remote device. - * @return the decrypted payload. - */ - @kotlin.jvm.Throws - suspend fun decryptMessage(ciphertext: String, messageType: Int, sessionId: String, theirDeviceIdentityKey: String): String? { - var payloadString: String? = null - - val olmSessionWrapper = getSessionForDevice(theirDeviceIdentityKey, sessionId) - - if (null != olmSessionWrapper) { - val olmMessage = OlmMessage() - olmMessage.mCipherText = ciphertext - olmMessage.mType = messageType.toLong() - - payloadString = - olmSessionWrapper.mutex.withLock { - olmSessionWrapper.olmSession.decryptMessage(olmMessage).also { - olmSessionWrapper.onMessageReceived(clock.epochMillis()) - } - } - olmSessionStore.storeSession(olmSessionWrapper, theirDeviceIdentityKey) - } - - return payloadString - } - - /** - * Determine if an incoming messages is a prekey message matching an existing session. - * - * @param theirDeviceIdentityKey the Curve25519 identity key for the remote device. - * @param sessionId the id of the active session. - * @param messageType message_type field from the received message. - * @param ciphertext the base64-encoded body from the received message. - * @return YES if the received message is a prekey message which matchesthe given session. - */ - fun matchesSession(theirDeviceIdentityKey: String, sessionId: String, messageType: Int, ciphertext: String): Boolean { - if (messageType != 0) { - return false - } - - val olmSessionWrapper = getSessionForDevice(theirDeviceIdentityKey, sessionId) - return null != olmSessionWrapper && olmSessionWrapper.olmSession.matchesInboundSession(ciphertext) - } - - // Outbound group session - - /** - * Generate a new outbound group session. - * - * @return the session id for the outbound session. - */ - fun createOutboundGroupSessionForRoom(roomId: String): String? { - var session: OlmOutboundGroupSession? = null - try { - session = OlmOutboundGroupSession() - outboundGroupSessionCache[session.sessionIdentifier()] = GroupSessionCacheItem(roomId, session) - store.storeCurrentOutboundGroupSessionForRoom(roomId, session) - return session.sessionIdentifier() - } catch (e: Exception) { - Timber.tag(loggerTag.value).e(e, "createOutboundGroupSession") - - session?.releaseSession() - } - - return null - } - - fun storeOutboundGroupSessionForRoom(roomId: String, sessionId: String) { - outboundGroupSessionCache[sessionId]?.let { - store.storeCurrentOutboundGroupSessionForRoom(roomId, it.groupSession) - } - } - - fun restoreOutboundGroupSessionForRoom(roomId: String): MXOutboundSessionInfo? { - val restoredOutboundGroupSession = store.getCurrentOutboundGroupSessionForRoom(roomId) - if (restoredOutboundGroupSession != null) { - val sessionId = restoredOutboundGroupSession.outboundGroupSession.sessionIdentifier() - // cache it - outboundGroupSessionCache[sessionId] = GroupSessionCacheItem(roomId, restoredOutboundGroupSession.outboundGroupSession) - - return MXOutboundSessionInfo( - sessionId = sessionId, - sharedWithHelper = SharedWithHelper(roomId, sessionId, store), - clock = clock, - creationTime = restoredOutboundGroupSession.creationTime, - sharedHistory = restoredOutboundGroupSession.sharedHistory - ) - } - return null - } - - fun discardOutboundGroupSessionForRoom(roomId: String) { - val toDiscard = outboundGroupSessionCache.filter { - it.value.groupId == roomId - } - toDiscard.forEach { (sessionId, cacheItem) -> - cacheItem.groupSession.releaseSession() - outboundGroupSessionCache.remove(sessionId) - } - store.storeCurrentOutboundGroupSessionForRoom(roomId, null) - } - - /** - * Get the current session key of an outbound group session. - * - * @param sessionId the id of the outbound group session. - * @return the base64-encoded secret key. - */ - fun getSessionKey(sessionId: String): String? { - if (sessionId.isNotEmpty()) { - try { - return outboundGroupSessionCache[sessionId]!!.groupSession.sessionKey() - } catch (e: Exception) { - Timber.tag(loggerTag.value).e(e, "## getSessionKey() : failed") - } - } - return null - } - - /** - * Get the current message index of an outbound group session. - * - * @param sessionId the id of the outbound group session. - * @return the current chain index. - */ - fun getMessageIndex(sessionId: String): Int { - return if (sessionId.isNotEmpty()) { - outboundGroupSessionCache[sessionId]!!.groupSession.messageIndex() - } else 0 - } - - /** - * Encrypt an outgoing message with an outbound group session. - * - * @param sessionId the id of the outbound group session. - * @param payloadString the payload to be encrypted and sent. - * @return ciphertext - */ - fun encryptGroupMessage(sessionId: String, payloadString: String): String? { - if (sessionId.isNotEmpty() && payloadString.isNotEmpty()) { - try { - return outboundGroupSessionCache[sessionId]!!.groupSession.encryptMessage(payloadString) - } catch (e: Throwable) { - Timber.tag(loggerTag.value).e(e, "## encryptGroupMessage() : failed") - } - } - return null - } - - // Inbound group session - - sealed interface AddSessionResult { - data class Imported(val ratchetIndex: Int) : AddSessionResult - abstract class Failure : AddSessionResult - object NotImported : Failure() - data class NotImportedHigherIndex(val newIndex: Int) : Failure() - } - - /** - * Add an inbound group session to the session store. - * - * @param sessionId the session identifier. - * @param sessionKey base64-encoded secret key. - * @param roomId the id of the room in which this session will be used. - * @param senderKey the base64-encoded curve25519 key of the sender. - * @param forwardingCurve25519KeyChain Devices involved in forwarding this session to us. - * @param keysClaimed Other keys the sender claims. - * @param exportFormat true if the megolm keys are in export format - * @param sharedHistory MSC3061, this key is sharable on invite - * @param trusted True if the key is coming from a trusted source - * @return true if the operation succeeds. - */ - fun addInboundGroupSession( - sessionId: String, - sessionKey: String, - roomId: String, - senderKey: String, - forwardingCurve25519KeyChain: List, - keysClaimed: Map, - exportFormat: Boolean, - sharedHistory: Boolean, - trusted: Boolean - ): AddSessionResult { - val candidateSession = tryOrNull("Failed to create inbound session in room $roomId") { - if (exportFormat) { - OlmInboundGroupSession.importSession(sessionKey) - } else { - OlmInboundGroupSession(sessionKey) - } - } ?: return AddSessionResult.NotImported.also { - Timber.tag(loggerTag.value).d("## addInboundGroupSession() : failed to import key candidate $senderKey/$sessionId") - } - - val existingSessionHolder = tryOrNull { getInboundGroupSession(sessionId, senderKey, roomId) } - val existingSession = existingSessionHolder?.wrapper - // If we have an existing one we should check if the new one is not better - if (existingSession != null) { - Timber.tag(loggerTag.value).d("## addInboundGroupSession() check if known session is better than candidate session") - try { - val existingFirstKnown = tryOrNull { existingSession.session.firstKnownIndex } ?: return AddSessionResult.NotImported.also { - // This is quite unexpected, could throw if native was released? - Timber.tag(loggerTag.value).e("## addInboundGroupSession() null firstKnownIndex on existing session") - candidateSession.releaseSession() - // Probably should discard it? - } - val newKnownFirstIndex = tryOrNull("Failed to get candidate first known index") { candidateSession.firstKnownIndex } - ?: return AddSessionResult.NotImported.also { - candidateSession.releaseSession() - Timber.tag(loggerTag.value).d("## addInboundGroupSession() : Failed to get new session index") - } - - val keyConnects = existingSession.session.connects(candidateSession) - if (!keyConnects) { - Timber.tag(loggerTag.value) - .e("## addInboundGroupSession() Unconnected key") - if (!trusted) { - // Ignore the not connecting unsafe, keep existing - Timber.tag(loggerTag.value) - .e("## addInboundGroupSession() Received unsafe unconnected key") - return AddSessionResult.NotImported - } - // else if the new one is safe and does not connect with existing, import the new one - } else { - // If our existing session is better we keep it - if (existingFirstKnown <= newKnownFirstIndex) { - val shouldUpdateTrust = trusted && (existingSession.sessionData.trusted != true) - Timber.tag(loggerTag.value).d("## addInboundGroupSession() : updateTrust for $sessionId") - if (shouldUpdateTrust) { - // the existing as a better index but the new one is trusted so update trust - inboundGroupSessionStore.updateToSafe(existingSessionHolder, sessionId, senderKey) - } - Timber.tag(loggerTag.value).d("## addInboundGroupSession() : ignore session our is better $senderKey/$sessionId") - candidateSession.releaseSession() - return AddSessionResult.NotImportedHigherIndex(newKnownFirstIndex.toInt()) - } - } - } catch (failure: Throwable) { - Timber.tag(loggerTag.value).e("## addInboundGroupSession() Failed to add inbound: ${failure.localizedMessage}") - candidateSession.releaseSession() - return AddSessionResult.NotImported - } - } - - Timber.tag(loggerTag.value).d("## addInboundGroupSession() : Candidate session should be added $senderKey/$sessionId") - - try { - if (candidateSession.sessionIdentifier() != sessionId) { - Timber.tag(loggerTag.value).e("## addInboundGroupSession : ERROR: Mismatched group session ID from senderKey: $senderKey") - candidateSession.releaseSession() - return AddSessionResult.NotImported - } - } catch (e: Throwable) { - candidateSession.releaseSession() - Timber.tag(loggerTag.value).e(e, "## addInboundGroupSession : sessionIdentifier() failed") - return AddSessionResult.NotImported - } - - val candidateSessionData = InboundGroupSessionData( - senderKey = senderKey, - roomId = roomId, - keysClaimed = keysClaimed, - forwardingCurve25519KeyChain = forwardingCurve25519KeyChain, - sharedHistory = sharedHistory, - trusted = trusted - ) - - val wrapper = MXInboundMegolmSessionWrapper( - candidateSession, - candidateSessionData - ) - if (existingSession != null) { - inboundGroupSessionStore.replaceGroupSession(existingSessionHolder, InboundGroupSessionHolder(wrapper), sessionId, senderKey) - } else { - inboundGroupSessionStore.storeInBoundGroupSession(InboundGroupSessionHolder(wrapper), sessionId, senderKey) - } - - return AddSessionResult.Imported(candidateSession.firstKnownIndex.toInt()) - } - - fun OlmInboundGroupSession.connects(other: OlmInboundGroupSession): Boolean { - return try { - val lowestCommonIndex = this.firstKnownIndex.coerceAtLeast(other.firstKnownIndex) - this.export(lowestCommonIndex) == other.export(lowestCommonIndex) - } catch (failure: Throwable) { - // native error? key disposed? - false - } - } - - /** - * Import an inbound group sessions to the session store. - * - * @param megolmSessionsData the megolm sessions data - * @return the successfully imported sessions. - */ - fun importInboundGroupSessions(megolmSessionsData: List): List { - val sessions = ArrayList(megolmSessionsData.size) - - for (megolmSessionData in megolmSessionsData) { - val sessionId = megolmSessionData.sessionId ?: continue - val senderKey = megolmSessionData.senderKey ?: continue - val roomId = megolmSessionData.roomId - - val candidateSessionToImport = try { - MXInboundMegolmSessionWrapper.newFromMegolmData(megolmSessionData, true) - } catch (e: Throwable) { - Timber.tag(loggerTag.value).e(e, "## importInboundGroupSession() : Failed to import session $senderKey/$sessionId") - continue - } - - val candidateOlmInboundGroupSession = candidateSessionToImport.session - val existingSessionHolder = tryOrNull { getInboundGroupSession(sessionId, senderKey, roomId) } - val existingSession = existingSessionHolder?.wrapper - - if (existingSession == null) { - // Session does not already exist, add it - Timber.tag(loggerTag.value).d("## importInboundGroupSession() : importing new megolm session $senderKey/$sessionId") - sessions.add(candidateSessionToImport) - } else { - Timber.tag(loggerTag.value).e("## importInboundGroupSession() : Update for megolm session $senderKey/$sessionId") - val existingFirstKnown = tryOrNull { existingSession.session.firstKnownIndex } - val candidateFirstKnownIndex = tryOrNull { candidateSessionToImport.session.firstKnownIndex } - - if (existingFirstKnown == null || candidateFirstKnownIndex == null) { - // should not happen? - candidateSessionToImport.session.releaseSession() - Timber.tag(loggerTag.value) - .w("## importInboundGroupSession() : Can't check session null index $existingFirstKnown/$candidateFirstKnownIndex") - } else { - if (existingFirstKnown <= candidateFirstKnownIndex) { - // Ignore this, keep existing - candidateOlmInboundGroupSession.releaseSession() - } else { - // update cache with better session - inboundGroupSessionStore.replaceGroupSession( - existingSessionHolder, - InboundGroupSessionHolder(candidateSessionToImport), - sessionId, - senderKey - ) - sessions.add(candidateSessionToImport) - } - } - } - } - - store.storeInboundGroupSessions(sessions) - - return sessions - } - - /** - * Decrypt a received message with an inbound group session. - * - * @param body the base64-encoded body of the encrypted message. - * @param roomId the room in which the message was received. - * @param timeline the id of the timeline where the event is decrypted. It is used to prevent replay attack. - * @param eventId the eventId of the message that will be decrypted - * @param sessionId the session identifier. - * @param senderKey the base64-encoded curve25519 key of the sender. - * @return the decrypting result. Null if the sessionId is unknown. - */ - @Throws(MXCryptoError::class) - suspend fun decryptGroupMessage( - body: String, - roomId: String, - timeline: String?, - eventId: String, - sessionId: String, - senderKey: String - ): OlmDecryptionResult { - val sessionHolder = getInboundGroupSession(sessionId, senderKey, roomId) - val wrapper = sessionHolder.wrapper - val inboundGroupSession = wrapper.session - if (roomId != wrapper.roomId) { - // Check that the room id matches the original one for the session. This stops - // the HS pretending a message was targeting a different room. - val reason = String.format(MXCryptoError.INBOUND_SESSION_MISMATCH_ROOM_ID_REASON, roomId, wrapper.roomId) - Timber.tag(loggerTag.value).e("## decryptGroupMessage() : $reason") - throw MXCryptoError.Base(MXCryptoError.ErrorType.INBOUND_SESSION_MISMATCH_ROOM_ID, reason) - } - val decryptResult = try { - sessionHolder.mutex.withLock { - inboundGroupSession.decryptMessage(body) - } - } catch (e: OlmException) { - Timber.tag(loggerTag.value).e(e, "## decryptGroupMessage () : decryptMessage failed") - throw MXCryptoError.OlmError(e) - } - - val messageIndexKey = senderKey + "|" + sessionId + "|" + roomId + "|" + decryptResult.mIndex - Timber.tag(loggerTag.value).v("##########################################################") - Timber.tag(loggerTag.value).v("## decryptGroupMessage() timeline: $timeline") - Timber.tag(loggerTag.value).v("## decryptGroupMessage() senderKey: $senderKey") - Timber.tag(loggerTag.value).v("## decryptGroupMessage() sessionId: $sessionId") - Timber.tag(loggerTag.value).v("## decryptGroupMessage() roomId: $roomId") - Timber.tag(loggerTag.value).v("## decryptGroupMessage() eventId: $eventId") - Timber.tag(loggerTag.value).v("## decryptGroupMessage() mIndex: ${decryptResult.mIndex}") - - if (timeline?.isNotBlank() == true) { - val replayAttackMap = inboundGroupSessionMessageIndexes.getOrPut(timeline) { mutableMapOf() } - if (replayAttackMap.contains(messageIndexKey) && replayAttackMap[messageIndexKey] != eventId) { - val reason = String.format(MXCryptoError.DUPLICATE_MESSAGE_INDEX_REASON, decryptResult.mIndex) - Timber.tag(loggerTag.value).e("## decryptGroupMessage() timelineId=$timeline: $reason") - throw MXCryptoError.Base(MXCryptoError.ErrorType.DUPLICATED_MESSAGE_INDEX, reason) - } - replayAttackMap[messageIndexKey] = eventId - } - val payload = try { - val adapter = MoshiProvider.providesMoshi().adapter(JSON_DICT_PARAMETERIZED_TYPE) - val payloadString = convertFromUTF8(decryptResult.mDecryptedMessage) - adapter.fromJson(payloadString) - } catch (e: Exception) { - Timber.tag(loggerTag.value).e("## decryptGroupMessage() : fails to parse the payload") - throw MXCryptoError.Base(MXCryptoError.ErrorType.BAD_DECRYPTED_FORMAT, MXCryptoError.BAD_DECRYPTED_FORMAT_TEXT_REASON) - } - - val verificationState = if (sessionHolder.wrapper.sessionData.trusted.orFalse()) { - // let's get info on the device - val sendingDevice = store.deviceWithIdentityKey(senderKey) - if (sendingDevice == null) { - MessageVerificationState.UNKNOWN_DEVICE - } else { - val isDeviceOwnerOfSession = sessionHolder.wrapper.sessionData.keysClaimed?.get("ed25519") == sendingDevice.fingerprint() - if (!isDeviceOwnerOfSession) { - // should it fail to decrypt here? - MessageVerificationState.UNSAFE_SOURCE - } else if (sendingDevice.isVerified) { - MessageVerificationState.VERIFIED - } else { - val isDeviceOwnerVerified = store.getCrossSigningInfo(sendingDevice.userId)?.isTrusted() ?: false - val isDeviceSignedByItsOwner = isDeviceSignByItsOwner(sendingDevice) - if (isDeviceSignedByItsOwner) { - if (isDeviceOwnerVerified) MessageVerificationState.VERIFIED - else MessageVerificationState.SIGNED_DEVICE_OF_UNVERIFIED_USER - } else { - if (isDeviceOwnerVerified) MessageVerificationState.UN_SIGNED_DEVICE_OF_VERIFIED_USER - else MessageVerificationState.UN_SIGNED_DEVICE - } - } - } - } else { - MessageVerificationState.UNSAFE_SOURCE - } - return OlmDecryptionResult( - payload, - wrapper.sessionData.keysClaimed, - senderKey, - wrapper.sessionData.forwardingCurve25519KeyChain, - isSafe = sessionHolder.wrapper.sessionData.trusted.orFalse(), - verificationState = verificationState, - ) - } - - private fun isDeviceSignByItsOwner(device: CryptoDeviceInfo): Boolean { - val otherKeys = store.getCrossSigningInfo(device.userId) ?: return false - val otherSSKSignature = device.signatures?.get(device.userId)?.get("ed25519:${otherKeys.selfSigningKey()?.unpaddedBase64PublicKey}") - ?: return false - - // Check bob's device is signed by bob's SSK - try { - crossSigningOlm.olmUtility.verifyEd25519Signature( - otherSSKSignature, - otherKeys.selfSigningKey()?.unpaddedBase64PublicKey, - device.canonicalSignable() - ) - return true - } catch (e: Throwable) { - return false - } - } - - /** - * Reset replay attack data for the given timeline. - * - * @param timeline the id of the timeline. - */ - fun resetReplayAttackCheckInTimeline(timeline: String?) { - if (null != timeline) { - inboundGroupSessionMessageIndexes.remove(timeline) - } - } - -// Utilities - - /** - * Verify an ed25519 signature on a JSON object. - * - * @param key the ed25519 key. - * @param jsonDictionary the JSON object which was signed. - * @param signature the base64-encoded signature to be checked. - * @throws Exception the exception - */ - @Throws(Exception::class) - fun verifySignature(key: String, jsonDictionary: Map, signature: String) { - // Check signature on the canonical version of the JSON - olmUtility!!.verifyEd25519Signature(signature, key, JsonCanonicalizer.getCanonicalJson(Map::class.java, jsonDictionary)) - } - - /** - * Calculate the SHA-256 hash of the input and encodes it as base64. - * - * @param message the message to hash. - * @return the base64-encoded hash value. - */ - fun sha256(message: String): String { - return olmUtility!!.sha256(convertToUTF8(message)) - } - - /** - * Search an OlmSession. - * - * @param theirDeviceIdentityKey the device key - * @param sessionId the session Id - * @return the olm session - */ - private fun getSessionForDevice(theirDeviceIdentityKey: String, sessionId: String): OlmSessionWrapper? { - // sanity check - return if (theirDeviceIdentityKey.isEmpty() || sessionId.isEmpty()) null else { - olmSessionStore.getDeviceSession(sessionId, theirDeviceIdentityKey) - } - } - - /** - * Extract an InboundGroupSession from the session store and do some check. - * inboundGroupSessionWithIdError describes the failure reason. - * - * @param sessionId the session identifier. - * @param senderKey the base64-encoded curve25519 key of the sender. - * @param roomId the room where the session is used. - * @return the inbound group session. - */ - fun getInboundGroupSession(sessionId: String?, senderKey: String?, roomId: String?): InboundGroupSessionHolder { - if (sessionId.isNullOrBlank() || senderKey.isNullOrBlank()) { - throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_SENDER_KEY, MXCryptoError.ERROR_MISSING_PROPERTY_REASON) - } - - val holder = inboundGroupSessionStore.getInboundGroupSession(sessionId, senderKey) - val session = holder?.wrapper - - if (session != null) { - // Check that the room id matches the original one for the session. This stops - // the HS pretending a message was targeting a different room. - if (roomId != session.roomId) { - val errorDescription = String.format(MXCryptoError.INBOUND_SESSION_MISMATCH_ROOM_ID_REASON, roomId, session.roomId) - Timber.tag(loggerTag.value).e("## getInboundGroupSession() : $errorDescription") - throw MXCryptoError.Base(MXCryptoError.ErrorType.INBOUND_SESSION_MISMATCH_ROOM_ID, errorDescription) - } else { - return holder - } - } else { - Timber.tag(loggerTag.value).w("## getInboundGroupSession() : UISI $sessionId") - throw MXCryptoError.Base(MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID, MXCryptoError.UNKNOWN_INBOUND_SESSION_ID_REASON) - } - } - - /** - * Determine if we have the keys for a given megolm session. - * - * @param roomId room in which the message was received - * @param senderKey base64-encoded curve25519 key of the sender - * @param sessionId session identifier - * @return true if the unbound session keys are known. - */ - fun hasInboundSessionKeys(roomId: String, senderKey: String, sessionId: String): Boolean { - return runCatching { getInboundGroupSession(sessionId, senderKey, roomId) }.isSuccess - } - - @VisibleForTesting - fun clearOlmSessionCache() { - olmSessionStore.clear() - } -} diff --git a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/MyDeviceInfoHolder.kt b/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/MyDeviceInfoHolder.kt deleted file mode 100644 index 4414c8f7be..0000000000 --- a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/MyDeviceInfoHolder.kt +++ /dev/null @@ -1,80 +0,0 @@ -/* - * Copyright 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. - */ - -package org.matrix.android.sdk.internal.crypto - -import org.matrix.android.sdk.api.auth.data.Credentials -import org.matrix.android.sdk.api.session.crypto.crosssigning.DeviceTrustLevel -import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo -import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore -import org.matrix.android.sdk.internal.session.SessionScope -import javax.inject.Inject - -@SessionScope -internal class MyDeviceInfoHolder @Inject constructor( - // The credentials, - credentials: Credentials, - // the crypto store - cryptoStore: IMXCryptoStore, - // Olm device - olmDevice: MXOlmDevice -) { - // Our device keys - /** - * my device info. - */ - val myDevice: CryptoDeviceInfo - - init { - - val keys = HashMap() - -// TODO it's a bit strange, why not load from DB? - if (!olmDevice.deviceEd25519Key.isNullOrEmpty()) { - keys["ed25519:" + credentials.deviceId] = olmDevice.deviceEd25519Key!! - } - - if (!olmDevice.deviceCurve25519Key.isNullOrEmpty()) { - keys["curve25519:" + credentials.deviceId] = olmDevice.deviceCurve25519Key!! - } - -// myDevice.keys = keys -// -// myDevice.algorithms = MXCryptoAlgorithms.supportedAlgorithms() - - // TODO hwo to really check cross signed status? - // - val crossSigned = cryptoStore.getMyCrossSigningInfo()?.masterKey()?.trustLevel?.locallyVerified ?: false -// myDevice.trustLevel = DeviceTrustLevel(crossSigned, true) - - myDevice = CryptoDeviceInfo( - credentials.deviceId, - credentials.userId, - keys = keys, - algorithms = MXCryptoAlgorithms.supportedAlgorithms(), - trustLevel = DeviceTrustLevel(crossSigned, true) - ) - - // Add our own deviceinfo to the store - val endToEndDevicesForUser = cryptoStore.getUserDevices(credentials.userId) - - val myDevices = endToEndDevicesForUser.orEmpty().toMutableMap() - - myDevices[myDevice.deviceId] = myDevice - - cryptoStore.storeUserDevices(credentials.userId, myDevices) - } -} diff --git a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/ObjectSigner.kt b/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/ObjectSigner.kt deleted file mode 100644 index 3f4b633ea0..0000000000 --- a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/ObjectSigner.kt +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright 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. - */ - -package org.matrix.android.sdk.internal.crypto - -import org.matrix.android.sdk.api.auth.data.Credentials -import javax.inject.Inject - -internal class ObjectSigner @Inject constructor( - private val credentials: Credentials, - private val olmDevice: MXOlmDevice -) { - - /** - * Sign Object. - * - * Example: - *
-     *     {
-     *         "[MY_USER_ID]": {
-     *             "ed25519:[MY_DEVICE_ID]": "sign(str)"
-     *         }
-     *     }
-     * 
- * - * @param strToSign the String to sign and to include in the Map - * @return a Map (see example) - */ - fun signObject(strToSign: String): Map> { - val result = HashMap>() - - val content = HashMap() - - content["ed25519:" + credentials.deviceId] = olmDevice.signMessage(strToSign) - ?: "" // null reported by rageshake if happens during logout - - result[credentials.userId] = content - - return result - } -} diff --git a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/OlmSessionStore.kt b/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/OlmSessionStore.kt deleted file mode 100644 index 4401a07192..0000000000 --- a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/OlmSessionStore.kt +++ /dev/null @@ -1,160 +0,0 @@ -/* - * Copyright 2022 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. - */ - -package org.matrix.android.sdk.internal.crypto - -import org.matrix.android.sdk.api.logger.LoggerTag -import org.matrix.android.sdk.internal.crypto.model.OlmSessionWrapper -import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore -import org.matrix.olm.OlmSession -import timber.log.Timber -import javax.inject.Inject - -private val loggerTag = LoggerTag("OlmSessionStore", LoggerTag.CRYPTO) - -/** - * Keep the used olm session in memory and load them from the data layer when needed. - * Access is synchronized for thread safety. - */ -internal class OlmSessionStore @Inject constructor(private val store: IMXCryptoStore) { - /** - * Map of device key to list of olm sessions (it is possible to have several active sessions with a device). - */ - private val olmSessions = HashMap>() - - /** - * Store a session between our own device and another device. - * This will be called after the session has been created but also every time it has been used - * in order to persist the correct state for next run - * @param olmSessionWrapper the end-to-end session. - * @param deviceKey the public key of the other device. - */ - @Synchronized - fun storeSession(olmSessionWrapper: OlmSessionWrapper, deviceKey: String) { - // This could be a newly created session or one that was just created - // Anyhow we should persist ratchet state for future app lifecycle - addNewSessionInCache(olmSessionWrapper, deviceKey) - store.storeSession(olmSessionWrapper, deviceKey) - } - - /** - * Get all the Olm Sessions we are sharing with the given device. - * - * @param deviceKey the public key of the other device. - * @return A set of sessionId, or empty if device is not known - */ - @Synchronized - fun getDeviceSessionIds(deviceKey: String): List { - // we need to get the persisted ids first - val persistedKnownSessions = store.getDeviceSessionIds(deviceKey) - .orEmpty() - .toMutableList() - // Do we have some in cache not yet persisted? - olmSessions.getOrPut(deviceKey) { mutableListOf() }.forEach { cached -> - getSafeSessionIdentifier(cached.olmSession)?.let { cachedSessionId -> - if (!persistedKnownSessions.contains(cachedSessionId)) { - // as it's in cache put in on top - persistedKnownSessions.add(0, cachedSessionId) - } - } - } - return persistedKnownSessions - } - - /** - * Retrieve an end-to-end session between our own device and another - * device. - * - * @param sessionId the session Id. - * @param deviceKey the public key of the other device. - * @return the session wrapper if found - */ - @Synchronized - fun getDeviceSession(sessionId: String, deviceKey: String): OlmSessionWrapper? { - // get from cache or load and add to cache - return internalGetSession(sessionId, deviceKey) - } - - /** - * Retrieve the last used sessionId, regarding `lastReceivedMessageTs`, or null if no session exist. - * - * @param deviceKey the public key of the other device. - * @return last used sessionId, or null if not found - */ - @Synchronized - fun getLastUsedSessionId(deviceKey: String): String? { - // We want to avoid to load in memory old session if possible - val lastPersistedUsedSession = store.getLastUsedSessionId(deviceKey) - var candidate = lastPersistedUsedSession?.let { internalGetSession(it, deviceKey) } - // we should check if we have one in cache with a higher last message received? - olmSessions[deviceKey].orEmpty().forEach { inCache -> - if (inCache.lastReceivedMessageTs > (candidate?.lastReceivedMessageTs ?: 0L)) { - candidate = inCache - } - } - - return candidate?.olmSession?.sessionIdentifier() - } - - /** - * Release all sessions and clear cache. - */ - @Synchronized - fun clear() { - olmSessions.entries.onEach { entry -> - entry.value.onEach { it.olmSession.releaseSession() } - } - olmSessions.clear() - } - - private fun internalGetSession(sessionId: String, deviceKey: String): OlmSessionWrapper? { - return getSessionInCache(sessionId, deviceKey) - ?: // deserialize from store - return store.getDeviceSession(sessionId, deviceKey)?.also { - addNewSessionInCache(it, deviceKey) - } - } - - private fun getSessionInCache(sessionId: String, deviceKey: String): OlmSessionWrapper? { - return olmSessions[deviceKey]?.firstOrNull { - getSafeSessionIdentifier(it.olmSession) == sessionId - } - } - - private fun getSafeSessionIdentifier(session: OlmSession): String? { - return try { - session.sessionIdentifier() - } catch (throwable: Throwable) { - Timber.tag(loggerTag.value).w("Failed to load sessionId from loaded olm session") - null - } - } - - private fun addNewSessionInCache(session: OlmSessionWrapper, deviceKey: String) { - val sessionId = getSafeSessionIdentifier(session.olmSession) ?: return - olmSessions.getOrPut(deviceKey) { mutableListOf() }.let { - val existing = it.firstOrNull { getSafeSessionIdentifier(it.olmSession) == sessionId } - it.add(session) - // remove and release if was there but with different instance - if (existing != null && existing.olmSession != session.olmSession) { - // mm not sure when this could happen - // anyhow we should remove and release the one known - it.remove(existing) - existing.olmSession.releaseSession() - } - } - } -} diff --git a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/OneTimeKeysUploader.kt b/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/OneTimeKeysUploader.kt deleted file mode 100644 index e6c45b12dc..0000000000 --- a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/OneTimeKeysUploader.kt +++ /dev/null @@ -1,250 +0,0 @@ -/* - * Copyright 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. - */ - -package org.matrix.android.sdk.internal.crypto - -import android.content.Context -import org.matrix.android.sdk.api.extensions.tryOrNull -import org.matrix.android.sdk.internal.crypto.model.MXKey -import org.matrix.android.sdk.internal.crypto.model.rest.KeysUploadBody -import org.matrix.android.sdk.internal.crypto.model.rest.KeysUploadResponse -import org.matrix.android.sdk.internal.crypto.tasks.UploadKeysTask -import org.matrix.android.sdk.internal.session.SessionScope -import org.matrix.android.sdk.internal.util.JsonCanonicalizer -import org.matrix.android.sdk.internal.util.time.Clock -import org.matrix.olm.OlmAccount -import timber.log.Timber -import javax.inject.Inject -import kotlin.math.floor -import kotlin.math.min - -// The spec recommend a 5mn delay, but due to federation -// or server downtime we give it a bit more time (1 hour) -private const val FALLBACK_KEY_FORGET_DELAY = 60 * 60_000L - -@SessionScope -internal class OneTimeKeysUploader @Inject constructor( - private val olmDevice: MXOlmDevice, - private val objectSigner: ObjectSigner, - private val uploadKeysTask: UploadKeysTask, - private val clock: Clock, - context: Context -) { - // tell if there is a OTK check in progress - private var oneTimeKeyCheckInProgress = false - - // last OTK check timestamp - private var lastOneTimeKeyCheck: Long = 0 - private var oneTimeKeyCount: Int? = null - - // Simple storage to remember when was uploaded the last fallback key - private val storage = context.getSharedPreferences("OneTimeKeysUploader_${olmDevice.deviceEd25519Key.hashCode()}", Context.MODE_PRIVATE) - - /** - * Stores the current one_time_key count which will be handled later (in a call of - * _onSyncCompleted). The count is e.g. coming from a /sync response. - * - * @param currentCount the new count - */ - fun updateOneTimeKeyCount(currentCount: Int) { - oneTimeKeyCount = currentCount - } - - fun needsNewFallback() { - if (olmDevice.generateFallbackKeyIfNeeded()) { - // As we generated a new one, it's already forgetting one - // so we can clear the last publish time - // (in case the network calls fails after to avoid calling forgetKey) - saveLastFallbackKeyPublishTime(0L) - } - } - - /** - * Check if the OTK must be uploaded. - */ - suspend fun maybeUploadOneTimeKeys() { - if (oneTimeKeyCheckInProgress) { - Timber.v("maybeUploadOneTimeKeys: already in progress") - return - } - if (clock.epochMillis() - lastOneTimeKeyCheck < ONE_TIME_KEY_UPLOAD_PERIOD) { - // we've done a key upload recently. - Timber.v("maybeUploadOneTimeKeys: executed too recently") - return - } - - oneTimeKeyCheckInProgress = true - - val oneTimeKeyCountFromSync = oneTimeKeyCount - ?: fetchOtkCount() // we don't have count from sync so get from server - ?: return Unit.also { - oneTimeKeyCheckInProgress = false - Timber.w("maybeUploadOneTimeKeys: Failed to get otk count from server") - } - - Timber.d("maybeUploadOneTimeKeys: otk count $oneTimeKeyCountFromSync , unpublished fallback key ${olmDevice.hasUnpublishedFallbackKey()}") - - lastOneTimeKeyCheck = clock.epochMillis() - - // We then check how many keys we can store in the Account object. - val maxOneTimeKeys = olmDevice.getMaxNumberOfOneTimeKeys() - - // Try to keep at most half that number on the server. This leaves the - // rest of the slots free to hold keys that have been claimed from the - // server but we haven't received a message for. - // If we run out of slots when generating new keys then olm will - // discard the oldest private keys first. This will eventually clean - // out stale private keys that won't receive a message. - val keyLimit = floor(maxOneTimeKeys / 2.0).toInt() - - // We need to keep a pool of one time public keys on the server so that - // other devices can start conversations with us. But we can only store - // a finite number of private keys in the olm Account object. - // To complicate things further then can be a delay between a device - // claiming a public one time key from the server and it sending us a - // message. We need to keep the corresponding private key locally until - // we receive the message. - // But that message might never arrive leaving us stuck with duff - // private keys clogging up our local storage. - // So we need some kind of engineering compromise to balance all of - // these factors. - tryOrNull("Unable to upload OTK") { - val uploadedKeys = uploadOTK(oneTimeKeyCountFromSync, keyLimit) - Timber.v("## uploadKeys() : success, $uploadedKeys key(s) sent") - } - oneTimeKeyCheckInProgress = false - - // Check if we need to forget a fallback key - val latestPublishedTime = getLastFallbackKeyPublishTime() - if (latestPublishedTime != 0L && clock.epochMillis() - latestPublishedTime > FALLBACK_KEY_FORGET_DELAY) { - // This should be called once you are reasonably certain that you will not receive any more messages - // that use the old fallback key - Timber.d("## forgetFallbackKey()") - olmDevice.forgetFallbackKey() - } - } - - private suspend fun fetchOtkCount(): Int? { - return tryOrNull("Unable to get OTK count") { - val result = uploadKeysTask.execute(UploadKeysTask.Params(KeysUploadBody())) - result.oneTimeKeyCountsForAlgorithm(MXKey.KEY_SIGNED_CURVE_25519_TYPE) - } - } - - /** - * Upload some the OTKs. - * - * @param keyCount the key count - * @param keyLimit the limit - * @return the number of uploaded keys - */ - private suspend fun uploadOTK(keyCount: Int, keyLimit: Int): Int { - if (keyLimit <= keyCount && !olmDevice.hasUnpublishedFallbackKey()) { - // If we don't need to generate any more keys then we are done. - return 0 - } - var keysThisLoop = 0 - if (keyLimit > keyCount) { - // Creating keys can be an expensive operation so we limit the - // number we generate in one go to avoid blocking the application - // for too long. - keysThisLoop = min(keyLimit - keyCount, ONE_TIME_KEY_GENERATION_MAX_NUMBER) - olmDevice.generateOneTimeKeys(keysThisLoop) - } - - // We check before sending if there is an unpublished key in order to saveLastFallbackKeyPublishTime if needed - val hadUnpublishedFallbackKey = olmDevice.hasUnpublishedFallbackKey() - val response = uploadOneTimeKeys(olmDevice.getOneTimeKeys()) - olmDevice.markKeysAsPublished() - if (hadUnpublishedFallbackKey) { - // It had an unpublished fallback key that was published just now - saveLastFallbackKeyPublishTime(clock.epochMillis()) - } - - if (response.hasOneTimeKeyCountsForAlgorithm(MXKey.KEY_SIGNED_CURVE_25519_TYPE)) { - // Maybe upload other keys - return keysThisLoop + - uploadOTK(response.oneTimeKeyCountsForAlgorithm(MXKey.KEY_SIGNED_CURVE_25519_TYPE), keyLimit) + - (if (hadUnpublishedFallbackKey) 1 else 0) - } else { - Timber.e("## uploadOTK() : response for uploading keys does not contain one_time_key_counts.signed_curve25519") - throw Exception("response for uploading keys does not contain one_time_key_counts.signed_curve25519") - } - } - - private fun saveLastFallbackKeyPublishTime(timeMillis: Long) { - storage.edit().putLong("last_fb_key_publish", timeMillis).apply() - } - - private fun getLastFallbackKeyPublishTime(): Long { - return storage.getLong("last_fb_key_publish", 0) - } - - /** - * Upload curve25519 one time keys. - */ - private suspend fun uploadOneTimeKeys(oneTimeKeys: Map>?): KeysUploadResponse { - val oneTimeJson = mutableMapOf() - - val curve25519Map = oneTimeKeys?.get(OlmAccount.JSON_KEY_ONE_TIME_KEY).orEmpty() - - curve25519Map.forEach { (key_id, value) -> - val k = mutableMapOf() - k["key"] = value - - // the key is also signed - val canonicalJson = JsonCanonicalizer.getCanonicalJson(Map::class.java, k) - - k["signatures"] = objectSigner.signObject(canonicalJson) - - oneTimeJson["signed_curve25519:$key_id"] = k - } - - val fallbackJson = mutableMapOf() - val fallbackCurve25519Map = olmDevice.getFallbackKey()?.get(OlmAccount.JSON_KEY_ONE_TIME_KEY).orEmpty() - fallbackCurve25519Map.forEach { (key_id, key) -> - val k = mutableMapOf() - k["key"] = key - k["fallback"] = true - val canonicalJson = JsonCanonicalizer.getCanonicalJson(Map::class.java, k) - k["signatures"] = objectSigner.signObject(canonicalJson) - - fallbackJson["signed_curve25519:$key_id"] = k - } - - // For now, we set the device id explicitly, as we may not be using the - // same one as used in login. - val uploadParams = UploadKeysTask.Params( - KeysUploadBody( - deviceKeys = null, - oneTimeKeys = oneTimeJson, - fallbackKeys = fallbackJson.takeIf { fallbackJson.isNotEmpty() } - ) - ) - return uploadKeysTask.executeRetry(uploadParams, 3) - } - - companion object { - // max number of keys to upload at once - // Creating keys can be an expensive operation so we limit the - // number we generate in one go to avoid blocking the application - // for too long. - private const val ONE_TIME_KEY_GENERATION_MAX_NUMBER = 5 - - // frequency with which to check & upload one-time keys - private const val ONE_TIME_KEY_UPLOAD_PERIOD = (60_000).toLong() // one minute - } -} diff --git a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/OutgoingKeyRequestManager.kt b/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/OutgoingKeyRequestManager.kt deleted file mode 100755 index 810699d933..0000000000 --- a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/OutgoingKeyRequestManager.kt +++ /dev/null @@ -1,526 +0,0 @@ -/* - * Copyright 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. - */ - -package org.matrix.android.sdk.internal.crypto - -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.SupervisorJob -import kotlinx.coroutines.asCoroutineDispatcher -import kotlinx.coroutines.cancel -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import org.matrix.android.sdk.api.MatrixCoroutineDispatchers -import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_MEGOLM -import org.matrix.android.sdk.api.crypto.MXCryptoConfig -import org.matrix.android.sdk.api.extensions.tryOrNull -import org.matrix.android.sdk.api.logger.LoggerTag -import org.matrix.android.sdk.api.session.crypto.OutgoingKeyRequest -import org.matrix.android.sdk.api.session.crypto.OutgoingRoomKeyRequestState -import org.matrix.android.sdk.api.session.crypto.model.GossipingToDeviceObject -import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap -import org.matrix.android.sdk.api.session.crypto.model.RoomKeyRequestBody -import org.matrix.android.sdk.api.session.crypto.model.RoomKeyShareRequest -import org.matrix.android.sdk.api.session.events.model.Event -import org.matrix.android.sdk.api.session.events.model.EventType -import org.matrix.android.sdk.api.session.events.model.content.EncryptedEventContent -import org.matrix.android.sdk.api.session.events.model.content.RoomKeyWithHeldContent -import org.matrix.android.sdk.api.session.events.model.toModel -import org.matrix.android.sdk.api.util.fromBase64 -import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore -import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask -import org.matrix.android.sdk.internal.di.SessionId -import org.matrix.android.sdk.internal.di.UserId -import org.matrix.android.sdk.internal.session.SessionScope -import org.matrix.android.sdk.internal.task.SemaphoreCoroutineSequencer -import timber.log.Timber -import java.util.Stack -import java.util.concurrent.Executors -import javax.inject.Inject -import kotlin.system.measureTimeMillis - -private val loggerTag = LoggerTag("OutgoingKeyRequestManager", LoggerTag.CRYPTO) - -/** - * This class is responsible for sending key requests to other devices when a message failed to decrypt. - * It's lifecycle is based on the sync pulse: - * - You can post queries for session, or report when you got a session - * - At the end of the sync (onSyncComplete) it will then process all the posted request and send to devices - * If a request failed it will be retried at the end of the next sync - */ -@SessionScope -internal class OutgoingKeyRequestManager @Inject constructor( - @SessionId private val sessionId: String, - @UserId private val myUserId: String, - private val cryptoStore: IMXCryptoStore, - private val coroutineDispatchers: MatrixCoroutineDispatchers, - private val cryptoConfig: MXCryptoConfig, - private val inboundGroupSessionStore: InboundGroupSessionStore, - private val sendToDeviceTask: SendToDeviceTask, - private val deviceListManager: DeviceListManager, - private val perSessionBackupQueryRateLimiter: PerSessionBackupQueryRateLimiter -) { - - private val dispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher() - private val outgoingRequestScope = CoroutineScope(SupervisorJob() + dispatcher) - private val sequencer = SemaphoreCoroutineSequencer() - - // We only have one active key request per session, so we don't request if it's already requested - // But it could make sense to check more the backup, as it's evolving. - // We keep a stack as we consider that the key requested last is more likely to be on screen? - private val requestDiscardedBecauseAlreadySentThatCouldBeTriedWithBackup = Stack>() - - fun requestKeyForEvent(event: Event, force: Boolean) { - val (targets, body) = getRoomKeyRequestTargetForEvent(event) ?: return - val index = ratchetIndexForMessage(event) ?: 0 - postRoomKeyRequest(body, targets, index, force) - } - - private fun getRoomKeyRequestTargetForEvent(event: Event): Pair>, RoomKeyRequestBody>? { - val sender = event.senderId ?: return null - val encryptedEventContent = event.content.toModel() ?: return null.also { - Timber.tag(loggerTag.value).e("getRoomKeyRequestTargetForEvent Failed to re-request key, null content") - } - if (encryptedEventContent.algorithm != MXCRYPTO_ALGORITHM_MEGOLM) return null - - val senderDevice = encryptedEventContent.deviceId - val recipients = if (cryptoConfig.limitRoomKeyRequestsToMyDevices) { - mapOf( - myUserId to listOf("*") - ) - } else { - if (event.senderId == myUserId) { - mapOf( - myUserId to listOf("*") - ) - } else { - // for the case where you share the key with a device that has a broken olm session - // The other user might Re-shares a megolm session key with devices if the key has already been - // sent to them. - mapOf( - myUserId to listOf("*"), - - // We might not have deviceId in the future due to https://github.com/matrix-org/matrix-spec-proposals/pull/3700 - // so in this case query to all - sender to listOf(senderDevice ?: "*") - ) - } - } - - val requestBody = RoomKeyRequestBody( - roomId = event.roomId, - algorithm = encryptedEventContent.algorithm, - senderKey = encryptedEventContent.senderKey, - sessionId = encryptedEventContent.sessionId - ) - return recipients to requestBody - } - - private fun ratchetIndexForMessage(event: Event): Int? { - val encryptedContent = event.content.toModel() ?: return null - if (encryptedContent.algorithm != MXCRYPTO_ALGORITHM_MEGOLM) return null - return encryptedContent.ciphertext?.fromBase64()?.inputStream()?.reader()?.let { - tryOrNull { - val megolmVersion = it.read() - if (megolmVersion != 3) return@tryOrNull null - /** Int tag */ - if (it.read() != 8) return@tryOrNull null - it.read() - } - } - } - - fun postRoomKeyRequest(requestBody: RoomKeyRequestBody, recipients: Map>, fromIndex: Int, force: Boolean = false) { - outgoingRequestScope.launch { - sequencer.post { - internalQueueRequest(requestBody, recipients, fromIndex, force) - } - } - } - - /** - * Typically called when we the session as been imported or received meanwhile. - */ - fun postCancelRequestForSessionIfNeeded(sessionId: String, roomId: String, senderKey: String, fromIndex: Int) { - outgoingRequestScope.launch { - sequencer.post { - internalQueueCancelRequest(sessionId, roomId, senderKey, fromIndex) - } - } - } - - fun onSelfCrossSigningTrustChanged(newTrust: Boolean) { - if (newTrust) { - // we were previously not cross signed, but we are now - // so there is now more chances to get better replies for existing request - // Let's forget about sent request so that next time we try to decrypt we will resend requests - // We don't resend all because we don't want to generate a bulk of traffic - outgoingRequestScope.launch { - sequencer.post { - cryptoStore.deleteOutgoingRoomKeyRequestInState(OutgoingRoomKeyRequestState.SENT) - } - - sequencer.post { - delay(1000) - perSessionBackupQueryRateLimiter.refreshBackupInfoIfNeeded(true) - } - } - } - } - - fun onRoomKeyForwarded( - sessionId: String, - algorithm: String, - roomId: String, - senderKey: String, - fromDevice: String?, - fromIndex: Int, - event: Event - ) { - Timber.tag(loggerTag.value).d("Key forwarded for $sessionId from ${event.senderId}|$fromDevice at index $fromIndex") - outgoingRequestScope.launch { - sequencer.post { - cryptoStore.updateOutgoingRoomKeyReply( - roomId = roomId, - sessionId = sessionId, - algorithm = algorithm, - senderKey = senderKey, - fromDevice = fromDevice, - // strip out encrypted stuff as it's just a trail? - event = event.copy( - type = event.getClearType(), - content = mapOf( - "chain_index" to fromIndex - ) - ) - ) - } - } - } - - fun onRoomKeyWithHeld( - sessionId: String, - algorithm: String, - roomId: String, - senderKey: String, - fromDevice: String?, - event: Event - ) { - outgoingRequestScope.launch { - sequencer.post { - Timber.tag(loggerTag.value).d("Withheld received for $sessionId from ${event.senderId}|$fromDevice") - Timber.tag(loggerTag.value).v("Withheld content ${event.getClearContent()}") - - // We want to store withheld code from the sender of the message (owner of the megolm session), not from - // other devices that might gossip the key. If not the initial reason might be overridden - // by a request to one of our session. - event.getClearContent().toModel()?.let { withheld -> - withContext(coroutineDispatchers.crypto) { - tryOrNull { - deviceListManager.downloadKeys(listOf(event.senderId ?: ""), false) - } - cryptoStore.getUserDeviceList(event.senderId ?: "") - .also { devices -> - Timber.tag(loggerTag.value) - .v("Withheld Devices for ${event.senderId} are ${devices.orEmpty().joinToString { it.identityKey() ?: "" }}") - } - ?.firstOrNull { - it.identityKey() == senderKey - } - }.also { - Timber.tag(loggerTag.value).v("Withheld device for sender key $senderKey is from ${it?.shortDebugString()}") - }?.let { - if (it.userId == event.senderId) { - if (fromDevice != null) { - if (it.deviceId == fromDevice) { - Timber.tag(loggerTag.value).v("Storing sender Withheld code ${withheld.code} for ${withheld.sessionId}") - cryptoStore.addWithHeldMegolmSession(withheld) - } - } else { - Timber.tag(loggerTag.value).v("Storing sender Withheld code ${withheld.code} for ${withheld.sessionId}") - cryptoStore.addWithHeldMegolmSession(withheld) - } - } - } - } - - // Here we store the replies from a given request - cryptoStore.updateOutgoingRoomKeyReply( - roomId = roomId, - sessionId = sessionId, - algorithm = algorithm, - senderKey = senderKey, - fromDevice = fromDevice, - event = event - ) - } - } - } - - /** - * Should be called after a sync, ideally if no catchup sync needed (as keys might arrive in those). - */ - fun requireProcessAllPendingKeyRequests() { - outgoingRequestScope.launch { - sequencer.post { - internalProcessPendingKeyRequests() - } - } - } - - private fun internalQueueCancelRequest(sessionId: String, roomId: String, senderKey: String, localKnownChainIndex: Int) { - // do we have known requests for that session?? - Timber.tag(loggerTag.value).v("Cancel Key Request if needed for $sessionId") - val knownRequest = cryptoStore.getOutgoingRoomKeyRequest( - algorithm = MXCRYPTO_ALGORITHM_MEGOLM, - roomId = roomId, - sessionId = sessionId, - senderKey = senderKey - ) - if (knownRequest.isEmpty()) return Unit.also { - Timber.tag(loggerTag.value).v("Handle Cancel Key Request for $sessionId -- Was not currently requested") - } - if (knownRequest.size > 1) { - // It's worth logging, there should be only one - Timber.tag(loggerTag.value).w("Found multiple requests for same sessionId $sessionId") - } - knownRequest.forEach { request -> - when (request.state) { - OutgoingRoomKeyRequestState.UNSENT -> { - if (request.fromIndex >= localKnownChainIndex) { - // we have a good index we can cancel - cryptoStore.deleteOutgoingRoomKeyRequest(request.requestId) - } - } - OutgoingRoomKeyRequestState.SENT -> { - // It was already sent, and index satisfied we can cancel - if (request.fromIndex >= localKnownChainIndex) { - cryptoStore.updateOutgoingRoomKeyRequestState(request.requestId, OutgoingRoomKeyRequestState.CANCELLATION_PENDING) - } - } - OutgoingRoomKeyRequestState.CANCELLATION_PENDING -> { - // It is already marked to be cancelled - } - OutgoingRoomKeyRequestState.CANCELLATION_PENDING_AND_WILL_RESEND -> { - if (request.fromIndex >= localKnownChainIndex) { - // we just want to cancel now - cryptoStore.updateOutgoingRoomKeyRequestState(request.requestId, OutgoingRoomKeyRequestState.CANCELLATION_PENDING) - } - } - OutgoingRoomKeyRequestState.SENT_THEN_CANCELED -> { - // was already canceled - // if we need a better index, should we resend? - } - } - } - } - - fun close() { - try { - outgoingRequestScope.cancel("User Terminate") - requestDiscardedBecauseAlreadySentThatCouldBeTriedWithBackup.clear() - } catch (failure: Throwable) { - Timber.tag(loggerTag.value).w("Failed to shutDown request manager") - } - } - - private fun internalQueueRequest(requestBody: RoomKeyRequestBody, recipients: Map>, fromIndex: Int, force: Boolean) { - if (!cryptoStore.isKeyGossipingEnabled()) { - // we might want to try backup? - if (requestBody.roomId != null && requestBody.sessionId != null) { - requestDiscardedBecauseAlreadySentThatCouldBeTriedWithBackup.push(requestBody.roomId to requestBody.sessionId) - } - Timber.tag(loggerTag.value).d("discarding request for ${requestBody.sessionId} as gossiping is disabled") - return - } - - Timber.tag(loggerTag.value).d("Queueing key request for ${requestBody.sessionId} force:$force") - val existing = cryptoStore.getOutgoingRoomKeyRequest(requestBody) - Timber.tag(loggerTag.value).v("Queueing key request exiting is ${existing?.state}") - when (existing?.state) { - null -> { - // create a new one - cryptoStore.getOrAddOutgoingRoomKeyRequest(requestBody, recipients, fromIndex) - } - OutgoingRoomKeyRequestState.UNSENT -> { - // nothing it's new or not yet handled - } - OutgoingRoomKeyRequestState.SENT -> { - // it was already requested - Timber.tag(loggerTag.value).d("The session ${requestBody.sessionId} is already requested") - if (force) { - // update to UNSENT - Timber.tag(loggerTag.value).d(".. force to request ${requestBody.sessionId}") - cryptoStore.updateOutgoingRoomKeyRequestState(existing.requestId, OutgoingRoomKeyRequestState.CANCELLATION_PENDING_AND_WILL_RESEND) - } else { - if (existing.roomId != null && existing.sessionId != null) { - requestDiscardedBecauseAlreadySentThatCouldBeTriedWithBackup.push(existing.roomId to existing.sessionId) - } - } - } - OutgoingRoomKeyRequestState.CANCELLATION_PENDING -> { - // request is canceled only if I got the keys so what to do here... - if (force) { - cryptoStore.updateOutgoingRoomKeyRequestState(existing.requestId, OutgoingRoomKeyRequestState.CANCELLATION_PENDING_AND_WILL_RESEND) - } - } - OutgoingRoomKeyRequestState.CANCELLATION_PENDING_AND_WILL_RESEND -> { - // It's already going to resend - } - OutgoingRoomKeyRequestState.SENT_THEN_CANCELED -> { - if (force) { - cryptoStore.deleteOutgoingRoomKeyRequest(existing.requestId) - cryptoStore.getOrAddOutgoingRoomKeyRequest(requestBody, recipients, fromIndex) - } - } - } - - if (existing != null && existing.fromIndex >= fromIndex) { - // update the required index - cryptoStore.updateOutgoingRoomKeyRequiredIndex(existing.requestId, fromIndex) - } - } - - private suspend fun internalProcessPendingKeyRequests() { - val toProcess = cryptoStore.getOutgoingRoomKeyRequests(OutgoingRoomKeyRequestState.pendingStates()) - Timber.tag(loggerTag.value).v("Processing all pending key requests (found ${toProcess.size} pending)") - - measureTimeMillis { - toProcess.forEach { - when (it.state) { - OutgoingRoomKeyRequestState.UNSENT -> handleUnsentRequest(it) - OutgoingRoomKeyRequestState.CANCELLATION_PENDING -> handleRequestToCancel(it) - OutgoingRoomKeyRequestState.CANCELLATION_PENDING_AND_WILL_RESEND -> handleRequestToCancelWillResend(it) - OutgoingRoomKeyRequestState.SENT_THEN_CANCELED, - OutgoingRoomKeyRequestState.SENT -> { - // these are filtered out - } - } - } - }.let { - Timber.tag(loggerTag.value).v("Finish processing pending key request in $it ms") - } - - val maxBackupCallsBySync = 60 - var currentCalls = 0 - measureTimeMillis { - while (requestDiscardedBecauseAlreadySentThatCouldBeTriedWithBackup.isNotEmpty() && currentCalls < maxBackupCallsBySync) { - requestDiscardedBecauseAlreadySentThatCouldBeTriedWithBackup.pop().let { (roomId, sessionId) -> - // we want to rate limit that somehow :/ - perSessionBackupQueryRateLimiter.tryFromBackupIfPossible(sessionId, roomId) - } - currentCalls++ - } - }.let { - Timber.tag(loggerTag.value).v("Finish querying backup in $it ms") - } - } - - private suspend fun handleUnsentRequest(request: OutgoingKeyRequest) { - // In order to avoid generating to_device traffic, we can first check if the key is backed up - Timber.tag(loggerTag.value).v("Handling unsent request for megolm session ${request.sessionId} in ${request.roomId}") - val sessionId = request.sessionId ?: return - val roomId = request.roomId ?: return - if (perSessionBackupQueryRateLimiter.tryFromBackupIfPossible(sessionId, roomId)) { - // let's see what's the index - val knownIndex = tryOrNull { - inboundGroupSessionStore.getInboundGroupSession(sessionId, request.requestBody?.senderKey ?: "") - ?.wrapper - ?.session - ?.firstKnownIndex - } - if (knownIndex != null && knownIndex <= request.fromIndex) { - // we found the key in backup with good enough index, so we can just mark as cancelled, no need to send request - Timber.tag(loggerTag.value).v("Megolm session $sessionId successfully restored from backup, do not send request") - cryptoStore.deleteOutgoingRoomKeyRequest(request.requestId) - return - } - } - - // we need to send the request - val toDeviceContent = RoomKeyShareRequest( - requestingDeviceId = cryptoStore.getDeviceId(), - requestId = request.requestId, - action = GossipingToDeviceObject.ACTION_SHARE_REQUEST, - body = request.requestBody - ) - val contentMap = MXUsersDevicesMap() - request.recipients.forEach { userToDeviceMap -> - userToDeviceMap.value.forEach { deviceId -> - contentMap.setObject(userToDeviceMap.key, deviceId, toDeviceContent) - } - } - - val params = SendToDeviceTask.Params( - eventType = EventType.ROOM_KEY_REQUEST, - contentMap = contentMap, - transactionId = request.requestId - ) - try { - withContext(coroutineDispatchers.io) { - sendToDeviceTask.executeRetry(params, 3) - } - Timber.tag(loggerTag.value).d("Key request sent for $sessionId in room $roomId to ${request.recipients}") - // The request was sent, so update state - cryptoStore.updateOutgoingRoomKeyRequestState(request.requestId, OutgoingRoomKeyRequestState.SENT) - // TODO update the audit trail - } catch (failure: Throwable) { - Timber.tag(loggerTag.value).v("Failed to request $sessionId targets:${request.recipients}") - } - } - - private suspend fun handleRequestToCancel(request: OutgoingKeyRequest): Boolean { - Timber.tag(loggerTag.value).v("handleRequestToCancel for megolm session ${request.sessionId}") - // we have to cancel this - val toDeviceContent = RoomKeyShareRequest( - requestingDeviceId = cryptoStore.getDeviceId(), - requestId = request.requestId, - action = GossipingToDeviceObject.ACTION_SHARE_CANCELLATION - ) - val contentMap = MXUsersDevicesMap() - request.recipients.forEach { userToDeviceMap -> - userToDeviceMap.value.forEach { deviceId -> - contentMap.setObject(userToDeviceMap.key, deviceId, toDeviceContent) - } - } - - val params = SendToDeviceTask.Params( - eventType = EventType.ROOM_KEY_REQUEST, - contentMap = contentMap, - transactionId = request.requestId - ) - return try { - withContext(coroutineDispatchers.io) { - sendToDeviceTask.executeRetry(params, 3) - } - // The request cancellation was sent, we don't delete yet because we want - // to keep trace of the sent replies - cryptoStore.updateOutgoingRoomKeyRequestState(request.requestId, OutgoingRoomKeyRequestState.SENT_THEN_CANCELED) - true - } catch (failure: Throwable) { - Timber.tag(loggerTag.value).v("Failed to cancel request ${request.requestId} for session $sessionId targets:${request.recipients}") - false - } - } - - private suspend fun handleRequestToCancelWillResend(request: OutgoingKeyRequest) { - if (handleRequestToCancel(request)) { - // this will create a new unsent request with no replies that will be process in the following call - cryptoStore.deleteOutgoingRoomKeyRequest(request.requestId) - request.requestBody?.let { cryptoStore.getOrAddOutgoingRoomKeyRequest(it, request.recipients, request.fromIndex) } - } - } -} diff --git a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/RoomDecryptorProvider.kt b/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/RoomDecryptorProvider.kt deleted file mode 100644 index 52e306edeb..0000000000 --- a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/RoomDecryptorProvider.kt +++ /dev/null @@ -1,106 +0,0 @@ -/* - * Copyright (c) 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. - */ - -package org.matrix.android.sdk.internal.crypto - -import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_MEGOLM -import org.matrix.android.sdk.api.session.crypto.NewSessionListener -import org.matrix.android.sdk.internal.crypto.algorithms.IMXDecrypting -import org.matrix.android.sdk.internal.crypto.algorithms.megolm.MXMegolmDecryptionFactory -import org.matrix.android.sdk.internal.crypto.algorithms.olm.MXOlmDecryptionFactory -import org.matrix.android.sdk.internal.session.SessionScope -import timber.log.Timber -import javax.inject.Inject - -@SessionScope -internal class RoomDecryptorProvider @Inject constructor( - private val olmDecryptionFactory: MXOlmDecryptionFactory, - private val megolmDecryptionFactory: MXMegolmDecryptionFactory -) { - - // A map from algorithm to MXDecrypting instance, for each room - private val roomDecryptors: MutableMap> = HashMap() - - private val newSessionListeners = ArrayList() - - fun addNewSessionListener(listener: NewSessionListener) { - if (!newSessionListeners.contains(listener)) newSessionListeners.add(listener) - } - - fun removeSessionListener(listener: NewSessionListener) { - newSessionListeners.remove(listener) - } - - /** - * Get a decryptor for a given room and algorithm. - * If we already have a decryptor for the given room and algorithm, return - * it. Otherwise try to instantiate it. - * - * @param roomId the room id - * @param algorithm the crypto algorithm - * @return the decryptor - * // TODO Create another method for the case of roomId is null - */ - fun getOrCreateRoomDecryptor(roomId: String?, algorithm: String?): IMXDecrypting? { - // sanity check - if (algorithm.isNullOrEmpty()) { - Timber.e("## getRoomDecryptor() : null algorithm") - return null - } - if (roomId != null && roomId.isNotEmpty()) { - synchronized(roomDecryptors) { - val decryptors = roomDecryptors.getOrPut(roomId) { mutableMapOf() } - val alg = decryptors[algorithm] - if (alg != null) { - return alg - } - } - } - val decryptingClass = MXCryptoAlgorithms.hasDecryptorClassForAlgorithm(algorithm) - if (decryptingClass) { - val alg = when (algorithm) { - MXCRYPTO_ALGORITHM_MEGOLM -> megolmDecryptionFactory.create().apply { - this.newSessionListener = object : NewSessionListener { - override fun onNewSession(roomId: String?, sessionId: String) { - // PR reviewer: the parameter has been renamed so is now in conflict with the parameter of getOrCreateRoomDecryptor - newSessionListeners.toList().forEach { - try { - it.onNewSession(roomId, sessionId) - } catch (ignore: Throwable) { - } - } - } - } - } - else -> olmDecryptionFactory.create() - } - if (!roomId.isNullOrEmpty()) { - synchronized(roomDecryptors) { - roomDecryptors[roomId]?.put(algorithm, alg) - } - } - return alg - } - return null - } - - fun getRoomDecryptor(roomId: String?, algorithm: String?): IMXDecrypting? { - if (roomId == null || algorithm == null) { - return null - } - return roomDecryptors[roomId]?.get(algorithm) - } -} diff --git a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/RoomEncryptorsStore.kt b/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/RoomEncryptorsStore.kt deleted file mode 100644 index 9f6714cc45..0000000000 --- a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/RoomEncryptorsStore.kt +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright 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. - */ - -package org.matrix.android.sdk.internal.crypto - -import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_MEGOLM -import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_OLM -import org.matrix.android.sdk.internal.crypto.algorithms.IMXEncrypting -import org.matrix.android.sdk.internal.crypto.algorithms.megolm.MXMegolmEncryptionFactory -import org.matrix.android.sdk.internal.crypto.algorithms.olm.MXOlmEncryptionFactory -import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore -import org.matrix.android.sdk.internal.session.SessionScope -import javax.inject.Inject - -@SessionScope -internal class RoomEncryptorsStore @Inject constructor( - private val cryptoStore: IMXCryptoStore, - private val megolmEncryptionFactory: MXMegolmEncryptionFactory, - private val olmEncryptionFactory: MXOlmEncryptionFactory, -) { - - // MXEncrypting instance for each room. - private val roomEncryptors = mutableMapOf() - - fun put(roomId: String, alg: IMXEncrypting) { - synchronized(roomEncryptors) { - roomEncryptors.put(roomId, alg) - } - } - - fun get(roomId: String): IMXEncrypting? { - return synchronized(roomEncryptors) { - val cache = roomEncryptors[roomId] - if (cache != null) { - return@synchronized cache - } else { - val alg: IMXEncrypting? = when (cryptoStore.getRoomAlgorithm(roomId)) { - MXCRYPTO_ALGORITHM_MEGOLM -> megolmEncryptionFactory.create(roomId) - MXCRYPTO_ALGORITHM_OLM -> olmEncryptionFactory.create(roomId) - else -> null - } - alg?.let { roomEncryptors.put(roomId, it) } - return@synchronized alg - } - } - } -} diff --git a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/SecretShareManager.kt b/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/SecretShareManager.kt deleted file mode 100644 index 56c181a306..0000000000 --- a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/SecretShareManager.kt +++ /dev/null @@ -1,344 +0,0 @@ -/* - * Copyright (c) 2022 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. - */ - -package org.matrix.android.sdk.internal.crypto - -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.launch -import kotlinx.coroutines.sync.Mutex -import kotlinx.coroutines.sync.withLock -import kotlinx.coroutines.withContext -import org.matrix.android.sdk.api.MatrixCoroutineDispatchers -import org.matrix.android.sdk.api.auth.data.Credentials -import org.matrix.android.sdk.api.logger.LoggerTag -import org.matrix.android.sdk.api.session.crypto.crosssigning.KEYBACKUP_SECRET_SSSS_NAME -import org.matrix.android.sdk.api.session.crypto.crosssigning.MASTER_KEY_SSSS_NAME -import org.matrix.android.sdk.api.session.crypto.crosssigning.SELF_SIGNING_KEY_SSSS_NAME -import org.matrix.android.sdk.api.session.crypto.crosssigning.USER_SIGNING_KEY_SSSS_NAME -import org.matrix.android.sdk.api.session.crypto.keyshare.GossipingRequestListener -import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap -import org.matrix.android.sdk.api.session.crypto.model.SecretShareRequest -import org.matrix.android.sdk.api.session.events.model.Event -import org.matrix.android.sdk.api.session.events.model.EventType -import org.matrix.android.sdk.api.session.events.model.content.SecretSendEventContent -import org.matrix.android.sdk.api.session.events.model.toModel -import org.matrix.android.sdk.internal.crypto.actions.EnsureOlmSessionsForDevicesAction -import org.matrix.android.sdk.internal.crypto.actions.MessageEncrypter -import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore -import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask -import org.matrix.android.sdk.internal.crypto.tasks.createUniqueTxnId -import org.matrix.android.sdk.internal.session.SessionScope -import org.matrix.android.sdk.internal.util.time.Clock -import timber.log.Timber -import javax.inject.Inject - -private val loggerTag = LoggerTag("SecretShareManager", LoggerTag.CRYPTO) - -@SessionScope -internal class SecretShareManager @Inject constructor( - private val credentials: Credentials, - private val cryptoStore: IMXCryptoStore, - private val cryptoCoroutineScope: CoroutineScope, - private val messageEncrypter: MessageEncrypter, - private val ensureOlmSessionsForDevicesAction: EnsureOlmSessionsForDevicesAction, - private val sendToDeviceTask: SendToDeviceTask, - private val coroutineDispatchers: MatrixCoroutineDispatchers, - private val clock: Clock, -) { - - companion object { - private const val SECRET_SHARE_WINDOW_DURATION = 5 * 60 * 1000 // 5 minutes - } - - /** - * Secret gossiping only occurs during a limited window period after interactive verification. - * We keep track of recent verification in memory for that purpose (no need to persist) - */ - private val recentlyVerifiedDevices = mutableMapOf() - private val verifMutex = Mutex() - - /** - * Secrets are exchanged as part of interactive verification, - * so we can just store in memory. - */ - private val outgoingSecretRequests = mutableListOf() - - // the listeners - private val gossipingRequestListeners: MutableSet = HashSet() - - fun addListener(listener: GossipingRequestListener) { - synchronized(gossipingRequestListeners) { - gossipingRequestListeners.add(listener) - } - } - - fun removeListener(listener: GossipingRequestListener) { - synchronized(gossipingRequestListeners) { - gossipingRequestListeners.remove(listener) - } - } - - /** - * Called when a session has been verified. - * This information can be used by the manager to decide whether or not to fullfill gossiping requests. - * This should be called as fast as possible after a successful self interactive verification - */ - fun onVerificationCompleteForDevice(deviceId: String) { - // For now we just keep an in memory cache - cryptoCoroutineScope.launch { - verifMutex.withLock { - recentlyVerifiedDevices[deviceId] = clock.epochMillis() - } - } - } - - suspend fun handleSecretRequest(toDevice: Event) { - val request = toDevice.getClearContent().toModel() - ?: return Unit.also { - Timber.tag(loggerTag.value) - .w("handleSecretRequest() : malformed request") - } - -// val (action, requestingDeviceId, requestId, secretName) = it - val secretName = request.secretName ?: return Unit.also { - Timber.tag(loggerTag.value) - .v("handleSecretRequest() : Missing secret name") - } - - val userId = toDevice.senderId ?: return Unit.also { - Timber.tag(loggerTag.value) - .v("handleSecretRequest() : Missing senderId") - } - - if (userId != credentials.userId) { - // secrets are only shared between our own devices - Timber.tag(loggerTag.value) - .e("Ignoring secret share request from other users $userId") - return - } - - val deviceId = request.requestingDeviceId - ?: return Unit.also { - Timber.tag(loggerTag.value) - .w("handleSecretRequest() : malformed request norequestingDeviceId ") - } - - if (request.requestingDeviceId == credentials.deviceId) { - return Unit.also { - Timber.tag(loggerTag.value) - .v("handleSecretRequest() : Ignore request from self device") - } - } - - val device = cryptoStore.getUserDevice(credentials.userId, deviceId) - ?: return Unit.also { - Timber.tag(loggerTag.value) - .e("Received secret share request from unknown device $deviceId") - } - - val isRequestingDeviceTrusted = device.isVerified - val isRecentInteractiveVerification = hasBeenVerifiedLessThanFiveMinutesFromNow(device.deviceId) - if (isRequestingDeviceTrusted && isRecentInteractiveVerification) { - // we can share the secret - - val secretValue = when (secretName) { - MASTER_KEY_SSSS_NAME -> cryptoStore.getCrossSigningPrivateKeys()?.master - SELF_SIGNING_KEY_SSSS_NAME -> cryptoStore.getCrossSigningPrivateKeys()?.selfSigned - USER_SIGNING_KEY_SSSS_NAME -> cryptoStore.getCrossSigningPrivateKeys()?.user - KEYBACKUP_SECRET_SSSS_NAME -> cryptoStore.getKeyBackupRecoveryKeyInfo()?.recoveryKey?.toBase64() - else -> null - } - if (secretValue == null) { - Timber.tag(loggerTag.value) - .i("The secret is unknown $secretName, passing to app layer") - val toList = synchronized(gossipingRequestListeners) { gossipingRequestListeners.toList() } - toList.onEach { listener -> - listener.onSecretShareRequest(request) - } - return - } - - val payloadJson = mapOf( - "type" to EventType.SEND_SECRET, - "content" to mapOf( - "request_id" to request.requestId, - "secret" to secretValue - ) - ) - - // Is it possible that we don't have an olm session? - val devicesByUser = mapOf(device.userId to listOf(device)) - val usersDeviceMap = try { - ensureOlmSessionsForDevicesAction.handle(devicesByUser) - } catch (failure: Throwable) { - Timber.tag(loggerTag.value) - .w("Can't share secret ${request.secretName}: Failed to establish olm session") - return - } - - val olmSessionResult = usersDeviceMap.getObject(device.userId, device.deviceId) - if (olmSessionResult?.sessionId == null) { - Timber.tag(loggerTag.value) - .w("secret share: no session with this device $deviceId, probably because there were no one-time keys") - return - } - - val encodedPayload = messageEncrypter.encryptMessage(payloadJson, listOf(device)) - val sendToDeviceMap = MXUsersDevicesMap() - sendToDeviceMap.setObject(device.userId, device.deviceId, encodedPayload) - val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, sendToDeviceMap) - try { - // raise the retries for secret - sendToDeviceTask.executeRetry(sendToDeviceParams, 6) - Timber.tag(loggerTag.value) - .i("successfully shared secret $secretName to ${device.shortDebugString()}") - // TODO add a trail for that in audit logs - } catch (failure: Throwable) { - Timber.tag(loggerTag.value) - .e(failure, "failed to send shared secret $secretName to ${device.shortDebugString()}") - } - } else { - Timber.tag(loggerTag.value) - .d(" Received secret share request from un-authorised device ${device.deviceId}") - } - } - - private suspend fun hasBeenVerifiedLessThanFiveMinutesFromNow(deviceId: String): Boolean { - val verifTimestamp = verifMutex.withLock { - recentlyVerifiedDevices[deviceId] - } ?: return false - - val age = clock.epochMillis() - verifTimestamp - - return age < SECRET_SHARE_WINDOW_DURATION - } - - suspend fun requestSecretTo(deviceId: String, secretName: String) { - val cryptoDeviceInfo = cryptoStore.getUserDevice(credentials.userId, deviceId) ?: return Unit.also { - Timber.tag(loggerTag.value) - .d("Can't request secret for $secretName unknown device $deviceId") - } - val toDeviceContent = SecretShareRequest( - requestingDeviceId = credentials.deviceId, - secretName = secretName, - requestId = createUniqueTxnId() - ) - - verifMutex.withLock { - outgoingSecretRequests.add(toDeviceContent) - } - - val contentMap = MXUsersDevicesMap() - contentMap.setObject(cryptoDeviceInfo.userId, cryptoDeviceInfo.deviceId, toDeviceContent) - - val params = SendToDeviceTask.Params( - eventType = EventType.REQUEST_SECRET, - contentMap = contentMap - ) - try { - withContext(coroutineDispatchers.io) { - sendToDeviceTask.execute(params) - } - Timber.tag(loggerTag.value) - .d("Secret request sent for $secretName to ${cryptoDeviceInfo.shortDebugString()}") - // TODO update the audit trail - } catch (failure: Throwable) { - Timber.tag(loggerTag.value) - .w("Failed to request secret $secretName to ${cryptoDeviceInfo.shortDebugString()}") - } - } - - suspend fun requestMissingSecrets() { - // quick implementation for backward compatibility with rust, will request all secrets to all own devices - val secretNames = listOf(MASTER_KEY_SSSS_NAME, SELF_SIGNING_KEY_SSSS_NAME, USER_SIGNING_KEY_SSSS_NAME, KEYBACKUP_SECRET_SSSS_NAME) - - secretNames.forEach { secretName -> - val toDeviceContent = SecretShareRequest( - requestingDeviceId = credentials.deviceId, - secretName = secretName, - requestId = createUniqueTxnId() - ) - - val contentMap = MXUsersDevicesMap() - contentMap.setObject(credentials.userId, "*", toDeviceContent) - - val params = SendToDeviceTask.Params( - eventType = EventType.REQUEST_SECRET, - contentMap = contentMap - ) - try { - withContext(coroutineDispatchers.io) { - sendToDeviceTask.execute(params) - } - Timber.tag(loggerTag.value) - .d("Secret request sent for $secretName") - } catch (failure: Throwable) { - Timber.tag(loggerTag.value) - .w("Failed to request secret $secretName") - } - } - } - - suspend fun onSecretSendReceived(toDevice: Event, handleGossip: ((name: String, value: String) -> Boolean)) { - Timber.tag(loggerTag.value) - .i("onSecretSend() from ${toDevice.senderId} : onSecretSendReceived ${toDevice.content?.get("sender_key")}") - if (!toDevice.isEncrypted()) { - // secret send messages must be encrypted - Timber.tag(loggerTag.value).e("onSecretSend() :Received unencrypted secret send event") - return - } - // no need to download keys, after a verification we already forced download - val sendingDevice = toDevice.getSenderKey()?.let { cryptoStore.deviceWithIdentityKey(it) } - if (sendingDevice == null) { - Timber.tag(loggerTag.value).e("onSecretSend() : Ignore secret from unknown device ${toDevice.getSenderKey()}") - return - } - - // Was that sent by us? - if (sendingDevice.userId != credentials.userId) { - Timber.tag(loggerTag.value).e("onSecretSend() : Ignore secret from other user ${toDevice.senderId}") - return - } - - if (!sendingDevice.isVerified) { - Timber.tag(loggerTag.value).e("onSecretSend() : Ignore secret from untrusted device ${toDevice.getSenderKey()}") - return - } - - val secretContent = toDevice.getClearContent().toModel() ?: return - - val existingRequest = verifMutex.withLock { - outgoingSecretRequests.firstOrNull { it.requestId == secretContent.requestId } - } - - // As per spec: - // Clients should ignore m.secret.send events received from devices that it did not send an m.secret.request event to. - if (existingRequest?.secretName == null) { - Timber.tag(loggerTag.value).i("onSecretSend() : Ignore secret that was not requested: ${secretContent.requestId}") - return - } - // we don't need to cancel the request as we only request to one device - // just forget about the request now - verifMutex.withLock { - outgoingSecretRequests.remove(existingRequest) - } - - if (!handleGossip(existingRequest.secretName, secretContent.secretValue)) { - // TODO Ask to application layer? - Timber.tag(loggerTag.value).v("onSecretSend() : secret not handled by SDK") - } - } -} diff --git a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/actions/EnsureOlmSessionsForDevicesAction.kt b/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/actions/EnsureOlmSessionsForDevicesAction.kt deleted file mode 100644 index 2a8e138f0b..0000000000 --- a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/actions/EnsureOlmSessionsForDevicesAction.kt +++ /dev/null @@ -1,182 +0,0 @@ -/* - * Copyright (c) 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. - */ - -package org.matrix.android.sdk.internal.crypto.actions - -import kotlinx.coroutines.sync.Mutex -import kotlinx.coroutines.sync.withLock -import kotlinx.coroutines.withContext -import org.matrix.android.sdk.api.MatrixCoroutineDispatchers -import org.matrix.android.sdk.api.logger.LoggerTag -import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo -import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap -import org.matrix.android.sdk.internal.crypto.MXOlmDevice -import org.matrix.android.sdk.internal.crypto.model.MXKey -import org.matrix.android.sdk.internal.crypto.model.MXOlmSessionResult -import org.matrix.android.sdk.internal.crypto.tasks.ClaimOneTimeKeysForUsersDeviceTask -import org.matrix.android.sdk.internal.session.SessionScope -import timber.log.Timber -import javax.inject.Inject - -private const val ONE_TIME_KEYS_RETRY_COUNT = 3 - -private val loggerTag = LoggerTag("EnsureOlmSessionsForDevicesAction", LoggerTag.CRYPTO) - -@SessionScope -internal class EnsureOlmSessionsForDevicesAction @Inject constructor( - private val olmDevice: MXOlmDevice, - private val coroutineDispatchers: MatrixCoroutineDispatchers, - private val oneTimeKeysForUsersDeviceTask: ClaimOneTimeKeysForUsersDeviceTask -) { - - private val ensureMutex = Mutex() - - /** - * We want to synchronize a bit here, because we are iterating to check existing olm session and - * also adding some. - */ - suspend fun handle(devicesByUser: Map>, force: Boolean = false): MXUsersDevicesMap { - ensureMutex.withLock { - val results = MXUsersDevicesMap() - val deviceList = devicesByUser.flatMap { it.value } - Timber.tag(loggerTag.value) - .d("ensure olm forced:$force for ${deviceList.joinToString { it.shortDebugString() }}") - val devicesToCreateSessionWith = mutableListOf() - if (force) { - // we take all devices and will query otk for them - devicesToCreateSessionWith.addAll(deviceList) - } else { - // only peek devices without active session - deviceList.forEach { deviceInfo -> - val deviceId = deviceInfo.deviceId - val userId = deviceInfo.userId - val key = deviceInfo.identityKey() ?: return@forEach Unit.also { - Timber.tag(loggerTag.value).w("Ignoring device ${deviceInfo.shortDebugString()} without identity key") - } - - // is there a session that as been already used? - val sessionId = olmDevice.getSessionId(key) - if (sessionId.isNullOrEmpty()) { - Timber.tag(loggerTag.value).d("Found no existing olm session ${deviceInfo.shortDebugString()} add to claim list") - devicesToCreateSessionWith.add(deviceInfo) - } else { - Timber.tag(loggerTag.value).d("using olm session $sessionId for (${deviceInfo.userId}|$deviceId)") - val olmSessionResult = MXOlmSessionResult(deviceInfo, sessionId) - results.setObject(userId, deviceId, olmSessionResult) - } - } - } - - if (devicesToCreateSessionWith.isEmpty()) { - // no session to create - return results - } - val usersDevicesToClaim = MXUsersDevicesMap().apply { - devicesToCreateSessionWith.forEach { - setObject(it.userId, it.deviceId, MXKey.KEY_SIGNED_CURVE_25519_TYPE) - } - } - - // Let's now claim one time keys - val claimParams = ClaimOneTimeKeysForUsersDeviceTask.Params(usersDevicesToClaim.map) - val oneTimeKeysForUsers = withContext(coroutineDispatchers.io) { - oneTimeKeysForUsersDeviceTask.executeRetry(claimParams, ONE_TIME_KEYS_RETRY_COUNT) - } - val oneTimeKeys = MXUsersDevicesMap() - for ((userId, mapByUserId) in oneTimeKeysForUsers.oneTimeKeys.orEmpty()) { - for ((deviceId, deviceKey) in mapByUserId) { - val mxKey = MXKey.from(deviceKey) - if (mxKey != null) { - oneTimeKeys.setObject(userId, deviceId, mxKey) - } else { - Timber.e("## claimOneTimeKeysForUsersDevices : fail to create a MXKey") - } - } - } - - // let now start olm session using the new otks - devicesToCreateSessionWith.forEach { deviceInfo -> - val userId = deviceInfo.userId - val deviceId = deviceInfo.deviceId - // Did we get an OTK - val oneTimeKey = oneTimeKeys.getObject(userId, deviceId) - if (oneTimeKey == null) { - Timber.tag(loggerTag.value).d("No otk for ${deviceInfo.shortDebugString()}") - } else if (oneTimeKey.type != MXKey.KEY_SIGNED_CURVE_25519_TYPE) { - Timber.tag(loggerTag.value).d("Bad otk type (${oneTimeKey.type}) for ${deviceInfo.shortDebugString()}") - } else { - val olmSessionId = verifyKeyAndStartSession(oneTimeKey, userId, deviceInfo) - if (olmSessionId != null) { - val olmSessionResult = MXOlmSessionResult(deviceInfo, olmSessionId) - results.setObject(userId, deviceId, olmSessionResult) - } else { - Timber - .tag(loggerTag.value) - .d("## CRYPTO | cant unwedge failed to create outbound ${deviceInfo.shortDebugString()}") - } - } - } - return results - } - } - - private fun verifyKeyAndStartSession(oneTimeKey: MXKey, userId: String, deviceInfo: CryptoDeviceInfo): String? { - var sessionId: String? = null - - val deviceId = deviceInfo.deviceId - val signKeyId = "ed25519:$deviceId" - val signature = oneTimeKey.signatureForUserId(userId, signKeyId) - - val fingerprint = deviceInfo.fingerprint() - if (!signature.isNullOrEmpty() && !fingerprint.isNullOrEmpty()) { - var isVerified = false - var errorMessage: String? = null - - try { - olmDevice.verifySignature(fingerprint, oneTimeKey.signalableJSONDictionary(), signature) - isVerified = true - } catch (e: Exception) { - Timber.tag(loggerTag.value).d( - e, "verifyKeyAndStartSession() : Verify error for otk: ${oneTimeKey.signalableJSONDictionary()}," + - " signature:$signature fingerprint:$fingerprint" - ) - Timber.tag(loggerTag.value).e( - "verifyKeyAndStartSession() : Verify error for ${deviceInfo.userId}|${deviceInfo.deviceId} " + - " - signable json ${oneTimeKey.signalableJSONDictionary()}" - ) - errorMessage = e.message - } - - // Check one-time key signature - if (isVerified) { - sessionId = deviceInfo.identityKey()?.let { identityKey -> - olmDevice.createOutboundSession(identityKey, oneTimeKey.value) - } - - if (sessionId.isNullOrEmpty()) { - // Possibly a bad key - Timber.tag(loggerTag.value).e("verifyKeyAndStartSession() : Error starting session with device $userId:$deviceId") - } else { - Timber.tag(loggerTag.value).d("verifyKeyAndStartSession() : Started new sessionId $sessionId for device $userId:$deviceId") - } - } else { - Timber.tag(loggerTag.value).e("verifyKeyAndStartSession() : Unable to verify otk signature for $userId:$deviceId: $errorMessage") - } - } - - return sessionId - } -} diff --git a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/actions/EnsureOlmSessionsForUsersAction.kt b/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/actions/EnsureOlmSessionsForUsersAction.kt deleted file mode 100644 index da09524668..0000000000 --- a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/actions/EnsureOlmSessionsForUsersAction.kt +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright 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. - */ - -package org.matrix.android.sdk.internal.crypto.actions - -import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap -import org.matrix.android.sdk.internal.crypto.MXOlmDevice -import org.matrix.android.sdk.internal.crypto.model.MXOlmSessionResult -import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore -import timber.log.Timber -import javax.inject.Inject - -internal class EnsureOlmSessionsForUsersAction @Inject constructor( - private val olmDevice: MXOlmDevice, - private val cryptoStore: IMXCryptoStore, - private val ensureOlmSessionsForDevicesAction: EnsureOlmSessionsForDevicesAction -) { - - /** - * Try to make sure we have established olm sessions for the given users. - * @param users a list of user ids. - */ - suspend fun handle(users: List): MXUsersDevicesMap { - Timber.v("## ensureOlmSessionsForUsers() : ensureOlmSessionsForUsers $users") - val devicesByUser = users.associateWith { userId -> - val devices = cryptoStore.getUserDevices(userId)?.values.orEmpty() - - devices.filter { - // Don't bother setting up session to ourself - it.identityKey() != olmDevice.deviceCurve25519Key && - // Don't bother setting up sessions with blocked users - !(it.trustLevel?.isVerified() ?: false) - } - } - return ensureOlmSessionsForDevicesAction.handle(devicesByUser) - } -} diff --git a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/actions/MegolmSessionDataImporter.kt b/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/actions/MegolmSessionDataImporter.kt deleted file mode 100644 index ad9c8eab51..0000000000 --- a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/actions/MegolmSessionDataImporter.kt +++ /dev/null @@ -1,132 +0,0 @@ -/* - * Copyright (c) 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. - */ - -package org.matrix.android.sdk.internal.crypto.actions - -import androidx.annotation.WorkerThread -import org.matrix.android.sdk.api.extensions.tryOrNull -import org.matrix.android.sdk.api.listeners.ProgressListener -import org.matrix.android.sdk.api.logger.LoggerTag -import org.matrix.android.sdk.api.session.crypto.model.ImportRoomKeysResult -import org.matrix.android.sdk.internal.crypto.MXOlmDevice -import org.matrix.android.sdk.internal.crypto.MegolmSessionData -import org.matrix.android.sdk.internal.crypto.OutgoingKeyRequestManager -import org.matrix.android.sdk.internal.crypto.RoomDecryptorProvider -import org.matrix.android.sdk.internal.crypto.algorithms.megolm.MXMegolmDecryption -import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore -import org.matrix.android.sdk.internal.util.time.Clock -import timber.log.Timber -import javax.inject.Inject - -private val loggerTag = LoggerTag("MegolmSessionDataImporter", LoggerTag.CRYPTO) - -internal class MegolmSessionDataImporter @Inject constructor( - private val olmDevice: MXOlmDevice, - private val roomDecryptorProvider: RoomDecryptorProvider, - private val outgoingKeyRequestManager: OutgoingKeyRequestManager, - private val cryptoStore: IMXCryptoStore, - private val clock: Clock, -) { - - /** - * Import a list of megolm session keys. - * Must be call on the crypto coroutine thread - * - * @param megolmSessionsData megolm sessions. - * @param fromBackup true if the imported keys are already backed up on the server. - * @param progressListener the progress listener - * @return import room keys result - */ - @WorkerThread - fun handle( - megolmSessionsData: List, - fromBackup: Boolean, - progressListener: ProgressListener? - ): ImportRoomKeysResult { - val t0 = clock.epochMillis() - val importedSession = mutableMapOf>>() - - val totalNumbersOfKeys = megolmSessionsData.size - var lastProgress = 0 - var totalNumbersOfImportedKeys = 0 - - progressListener?.onProgress(0, 100) - val olmInboundGroupSessionWrappers = olmDevice.importInboundGroupSessions(megolmSessionsData) - - megolmSessionsData.forEachIndexed { cpt, megolmSessionData -> - val decrypting = roomDecryptorProvider.getOrCreateRoomDecryptor(megolmSessionData.roomId, megolmSessionData.algorithm) - - if (null != decrypting) { - try { - val sessionId = megolmSessionData.sessionId ?: return@forEachIndexed - val senderKey = megolmSessionData.senderKey ?: return@forEachIndexed - val roomId = megolmSessionData.roomId ?: return@forEachIndexed - Timber.tag(loggerTag.value).v("## importRoomKeys retrieve senderKey ${megolmSessionData.senderKey} sessionId $sessionId") - - importedSession.getOrPut(roomId) { mutableMapOf() } - .getOrPut(senderKey) { mutableListOf() } - .add(sessionId) - totalNumbersOfImportedKeys++ - - // cancel any outstanding room key requests for this session - - Timber.tag(loggerTag.value).d("Imported megolm session $sessionId from backup=$fromBackup in ${megolmSessionData.roomId}") - outgoingKeyRequestManager.postCancelRequestForSessionIfNeeded( - sessionId, - roomId, - senderKey, - tryOrNull { - olmInboundGroupSessionWrappers - .firstOrNull { it.session.sessionIdentifier() == megolmSessionData.sessionId } - ?.session?.firstKnownIndex - ?.toInt() - } ?: 0 - ) - - // Have another go at decrypting events sent with this session - when (decrypting) { - is MXMegolmDecryption -> { - decrypting.onNewSession(megolmSessionData.roomId, senderKey, sessionId) - } - } - } catch (e: Exception) { - Timber.tag(loggerTag.value).e(e, "## importRoomKeys() : onNewSession failed") - } - } - - if (progressListener != null) { - val progress = 100 * (cpt + 1) / totalNumbersOfKeys - - if (lastProgress != progress) { - lastProgress = progress - - progressListener.onProgress(progress, 100) - } - } - } - - // Do not back up the key if it comes from a backup recovery - if (fromBackup) { - cryptoStore.markBackupDoneForInboundGroupSessions(olmInboundGroupSessionWrappers) - } - - val t1 = clock.epochMillis() - - Timber.tag(loggerTag.value).v("## importMegolmSessionsData : sessions import " + (t1 - t0) + " ms (" + megolmSessionsData.size + " sessions)") - - return ImportRoomKeysResult(totalNumbersOfKeys, totalNumbersOfImportedKeys, importedSession) - } -} diff --git a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/actions/MessageEncrypter.kt b/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/actions/MessageEncrypter.kt deleted file mode 100644 index eff2132820..0000000000 --- a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/actions/MessageEncrypter.kt +++ /dev/null @@ -1,89 +0,0 @@ -/* - * Copyright 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. - */ - -package org.matrix.android.sdk.internal.crypto.actions - -import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_OLM -import org.matrix.android.sdk.api.logger.LoggerTag -import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo -import org.matrix.android.sdk.api.session.events.model.Content -import org.matrix.android.sdk.internal.crypto.MXOlmDevice -import org.matrix.android.sdk.internal.crypto.model.rest.EncryptedMessage -import org.matrix.android.sdk.internal.di.DeviceId -import org.matrix.android.sdk.internal.di.UserId -import org.matrix.android.sdk.internal.util.JsonCanonicalizer -import org.matrix.android.sdk.internal.util.convertToUTF8 -import timber.log.Timber -import javax.inject.Inject - -private val loggerTag = LoggerTag("MessageEncrypter", LoggerTag.CRYPTO) - -internal class MessageEncrypter @Inject constructor( - @UserId - private val userId: String, - @DeviceId - private val deviceId: String?, - private val olmDevice: MXOlmDevice -) { - /** - * Encrypt an event payload for a list of devices. - * This method must be called from the getCryptoHandler() thread. - * - * @param payloadFields fields to include in the encrypted payload. - * @param deviceInfos list of device infos to encrypt for. - * @return the content for an m.room.encrypted event. - */ - suspend fun encryptMessage(payloadFields: Content, deviceInfos: List): EncryptedMessage { - val deviceInfoParticipantKey = deviceInfos.associateBy { it.identityKey()!! } - - val payloadJson = payloadFields.toMutableMap() - - payloadJson["sender"] = userId - payloadJson["sender_device"] = deviceId!! - - // Include the Ed25519 key so that the recipient knows what - // device this message came from. - // We don't need to include the curve25519 key since the - // recipient will already know this from the olm headers. - // When combined with the device keys retrieved from the - // homeserver signed by the ed25519 key this proves that - // the curve25519 key and the ed25519 key are owned by - // the same device. - payloadJson["keys"] = mapOf("ed25519" to olmDevice.deviceEd25519Key!!) - - val ciphertext = mutableMapOf() - - for ((deviceKey, deviceInfo) in deviceInfoParticipantKey) { - val sessionId = olmDevice.getSessionId(deviceKey) - - if (!sessionId.isNullOrEmpty()) { - Timber.tag(loggerTag.value).d("Using sessionid $sessionId for device $deviceKey") - - payloadJson["recipient"] = deviceInfo.userId - payloadJson["recipient_keys"] = mapOf("ed25519" to deviceInfo.fingerprint()!!) - - val payloadString = convertToUTF8(JsonCanonicalizer.getCanonicalJson(Map::class.java, payloadJson)) - ciphertext[deviceKey] = olmDevice.encryptMessage(deviceKey, sessionId, payloadString)!! - } - } - - return EncryptedMessage( - algorithm = MXCRYPTO_ALGORITHM_OLM, - senderKey = olmDevice.deviceCurve25519Key, - cipherText = ciphertext - ) - } -} diff --git a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/actions/SetDeviceVerificationAction.kt b/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/actions/SetDeviceVerificationAction.kt deleted file mode 100644 index aec082e003..0000000000 --- a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/actions/SetDeviceVerificationAction.kt +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright (c) 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. - */ - -package org.matrix.android.sdk.internal.crypto.actions - -import org.matrix.android.sdk.api.session.crypto.crosssigning.DeviceTrustLevel -import org.matrix.android.sdk.internal.crypto.keysbackup.DefaultKeysBackupService -import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore -import org.matrix.android.sdk.internal.di.UserId -import timber.log.Timber -import javax.inject.Inject - -internal class SetDeviceVerificationAction @Inject constructor( - private val cryptoStore: IMXCryptoStore, - @UserId private val userId: String, - private val defaultKeysBackupService: DefaultKeysBackupService -) { - - suspend fun handle(trustLevel: DeviceTrustLevel, userId: String, deviceId: String) { - val device = cryptoStore.getUserDevice(userId, deviceId) - - // Sanity check - if (null == device) { - Timber.w("## setDeviceVerification() : Unknown device $userId:$deviceId") - return - } - - if (device.isVerified != trustLevel.isVerified()) { - if (userId == this.userId) { - // If one of the user's own devices is being marked as verified / unverified, - // check the key backup status, since whether or not we use this depends on - // whether it has a signature from a verified device - defaultKeysBackupService.checkAndStartKeysBackup() - } - } - - if (device.trustLevel != trustLevel) { - device.trustLevel = trustLevel - cryptoStore.setDeviceTrust(userId, deviceId, trustLevel.crossSigningVerified, trustLevel.locallyVerified) - } - } -} diff --git a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/algorithms/IMXDecrypting.kt b/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/algorithms/IMXDecrypting.kt deleted file mode 100644 index 13e7ecba92..0000000000 --- a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/algorithms/IMXDecrypting.kt +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright (c) 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. - */ - -package org.matrix.android.sdk.internal.crypto.algorithms - -import org.matrix.android.sdk.api.session.crypto.MXCryptoError -import org.matrix.android.sdk.api.session.crypto.model.MXEventDecryptionResult -import org.matrix.android.sdk.api.session.events.model.Event -import org.matrix.android.sdk.internal.crypto.keysbackup.DefaultKeysBackupService - -/** - * An interface for decrypting data. - */ -internal interface IMXDecrypting { - - /** - * Decrypt an event. - * - * @param event the raw event. - * @param timeline the id of the timeline where the event is decrypted. It is used to prevent replay attack. - * @return the decryption information, or an error - */ - @Throws(MXCryptoError::class) - suspend fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult - - /** - * Handle a key event. - * - * @param event the key event. - * @param defaultKeysBackupService the keys backup service - * @param forceAccept the keys backup service - */ - suspend fun onRoomKeyEvent(event: Event, defaultKeysBackupService: DefaultKeysBackupService, forceAccept: Boolean = false) {} -} diff --git a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/algorithms/IMXEncrypting.kt b/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/algorithms/IMXEncrypting.kt deleted file mode 100644 index c585ac42c3..0000000000 --- a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/algorithms/IMXEncrypting.kt +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright (c) 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. - */ - -package org.matrix.android.sdk.internal.crypto.algorithms - -import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo -import org.matrix.android.sdk.api.session.events.model.Content -import org.matrix.android.sdk.internal.crypto.InboundGroupSessionHolder - -/** - * An interface for encrypting data. - */ -internal interface IMXEncrypting { - - /** - * Encrypt an event content according to the configuration of the room. - * - * @param eventContent the content of the event. - * @param eventType the type of the event. - * @param userIds the room members the event will be sent to. - * @return the encrypted content - */ - suspend fun encryptEventContent(eventContent: Content, eventType: String, userIds: List): Content - - suspend fun shareHistoryKeysWithDevice(inboundSessionWrapper: InboundGroupSessionHolder, deviceInfo: CryptoDeviceInfo) {} -} diff --git a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/algorithms/IMXGroupEncryption.kt b/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/algorithms/IMXGroupEncryption.kt deleted file mode 100644 index 69f8e5600b..0000000000 --- a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/algorithms/IMXGroupEncryption.kt +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright (c) 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. - */ - -package org.matrix.android.sdk.internal.crypto.algorithms - -internal interface IMXGroupEncryption { - - /** - * In Megolm, each recipient maintains a record of the ratchet value which allows - * them to decrypt any messages sent in the session after the corresponding point - * in the conversation. If this value is compromised, an attacker can similarly - * decrypt past messages which were encrypted by a key derived from the - * compromised or subsequent ratchet values. This gives 'partial' forward - * secrecy. - * - * To mitigate this issue, the application should offer the user the option to - * discard historical conversations, by winding forward any stored ratchet values, - * or discarding sessions altogether. - */ - fun discardSessionKey() - - suspend fun preshareKey(userIds: List) - - /** - * Re-shares a session key with devices if the key has already been - * sent to them. - * - * @param groupSessionId The id of the outbound session to share. - * @param userId The id of the user who owns the target device. - * @param deviceId The id of the target device. - * @param senderKey The key of the originating device for the session. - * - * @return true in case of success - */ - suspend fun reshareKey( - groupSessionId: String, - userId: String, - deviceId: String, - senderKey: String - ): Boolean -} diff --git a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmDecryption.kt b/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmDecryption.kt deleted file mode 100644 index 8721374244..0000000000 --- a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmDecryption.kt +++ /dev/null @@ -1,364 +0,0 @@ -/* - * Copyright (c) 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. - */ - -package org.matrix.android.sdk.internal.crypto.algorithms.megolm - -import dagger.Lazy -import org.matrix.android.sdk.api.crypto.MXCryptoConfig -import org.matrix.android.sdk.api.logger.LoggerTag -import org.matrix.android.sdk.api.session.crypto.MXCryptoError -import org.matrix.android.sdk.api.session.crypto.NewSessionListener -import org.matrix.android.sdk.api.session.crypto.model.ForwardedRoomKeyContent -import org.matrix.android.sdk.api.session.crypto.model.MXEventDecryptionResult -import org.matrix.android.sdk.api.session.events.model.Event -import org.matrix.android.sdk.api.session.events.model.EventType -import org.matrix.android.sdk.api.session.events.model.content.EncryptedEventContent -import org.matrix.android.sdk.api.session.events.model.content.RoomKeyContent -import org.matrix.android.sdk.api.session.events.model.toModel -import org.matrix.android.sdk.internal.crypto.MXOlmDevice -import org.matrix.android.sdk.internal.crypto.OutgoingKeyRequestManager -import org.matrix.android.sdk.internal.crypto.algorithms.IMXDecrypting -import org.matrix.android.sdk.internal.crypto.keysbackup.DefaultKeysBackupService -import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore -import org.matrix.android.sdk.internal.session.StreamEventsManager -import org.matrix.android.sdk.internal.util.time.Clock -import timber.log.Timber - -private val loggerTag = LoggerTag("MXMegolmDecryption", LoggerTag.CRYPTO) - -internal class MXMegolmDecryption( - private val olmDevice: MXOlmDevice, - private val myUserId: String, - private val outgoingKeyRequestManager: OutgoingKeyRequestManager, - private val cryptoStore: IMXCryptoStore, - private val liveEventManager: Lazy, - private val unrequestedForwardManager: UnRequestedForwardManager, - private val cryptoConfig: MXCryptoConfig, - private val clock: Clock, -) : IMXDecrypting { - - var newSessionListener: NewSessionListener? = null - - /** - * Events which we couldn't decrypt due to unknown sessions / indexes: map from - * senderKey|sessionId to timelines to list of MatrixEvents. - */ -// private var pendingEvents: MutableMap>> = HashMap() - - @Throws(MXCryptoError::class) - override suspend fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult { - return decryptEvent(event, timeline, true) - } - - @Throws(MXCryptoError::class) - private suspend fun decryptEvent(event: Event, timeline: String, requestKeysOnFail: Boolean): MXEventDecryptionResult { - Timber.tag(loggerTag.value).v("decryptEvent ${event.eventId}, requestKeysOnFail:$requestKeysOnFail") - if (event.roomId.isNullOrBlank()) { - throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_FIELDS, MXCryptoError.MISSING_FIELDS_REASON) - } - - val encryptedEventContent = event.content.toModel() - ?: throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_FIELDS, MXCryptoError.MISSING_FIELDS_REASON) - - if (encryptedEventContent.senderKey.isNullOrBlank() || - encryptedEventContent.sessionId.isNullOrBlank() || - encryptedEventContent.ciphertext.isNullOrBlank()) { - throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_FIELDS, MXCryptoError.MISSING_FIELDS_REASON) - } - - return runCatching { - olmDevice.decryptGroupMessage( - encryptedEventContent.ciphertext, - event.roomId, - timeline, - eventId = event.eventId.orEmpty(), - encryptedEventContent.sessionId, - encryptedEventContent.senderKey - ) - } - .fold( - { olmDecryptionResult -> - // the decryption succeeds - if (olmDecryptionResult.payload != null) { - MXEventDecryptionResult( - clearEvent = olmDecryptionResult.payload, - senderCurve25519Key = olmDecryptionResult.senderKey, - claimedEd25519Key = olmDecryptionResult.keysClaimed?.get("ed25519"), - forwardingCurve25519KeyChain = olmDecryptionResult.forwardingCurve25519KeyChain - .orEmpty(), - messageVerificationState = olmDecryptionResult.verificationState, - ).also { - liveEventManager.get().dispatchLiveEventDecrypted(event, it) - } - } else { - throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_FIELDS, MXCryptoError.MISSING_FIELDS_REASON) - } - }, - { throwable -> - liveEventManager.get().dispatchLiveEventDecryptionFailed(event, throwable) - if (throwable is MXCryptoError.OlmError) { - // TODO Check the value of .message - if (throwable.olmException.message == "UNKNOWN_MESSAGE_INDEX") { - // So we know that session, but it's ratcheted and we can't decrypt at that index - - if (requestKeysOnFail) { - requestKeysForEvent(event) - } - // Check if partially withheld - val withHeldInfo = cryptoStore.getWithHeldMegolmSession(event.roomId, encryptedEventContent.sessionId) - if (withHeldInfo != null) { - // Encapsulate as withHeld exception - throw MXCryptoError.Base( - MXCryptoError.ErrorType.KEYS_WITHHELD, - withHeldInfo.code?.value ?: "", - withHeldInfo.reason - ) - } - - throw MXCryptoError.Base( - MXCryptoError.ErrorType.UNKNOWN_MESSAGE_INDEX, - "UNKNOWN_MESSAGE_INDEX", - null - ) - } - - val reason = String.format(MXCryptoError.OLM_REASON, throwable.olmException.message) - val detailedReason = String.format(MXCryptoError.DETAILED_OLM_REASON, encryptedEventContent.ciphertext, reason) - - throw MXCryptoError.Base( - MXCryptoError.ErrorType.OLM, - reason, - detailedReason - ) - } - if (throwable is MXCryptoError.Base) { - if (throwable.errorType == MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID) { - // Check if it was withheld by sender to enrich error code - val withHeldInfo = cryptoStore.getWithHeldMegolmSession(event.roomId, encryptedEventContent.sessionId) - if (withHeldInfo != null) { - if (requestKeysOnFail) { - requestKeysForEvent(event) - } - // Encapsulate as withHeld exception - throw MXCryptoError.Base( - MXCryptoError.ErrorType.KEYS_WITHHELD, - withHeldInfo.code?.value ?: "", - withHeldInfo.reason - ) - } - - if (requestKeysOnFail) { - requestKeysForEvent(event) - } - } - } - throw throwable - } - ) - } - - /** - * Helper for the real decryptEvent and for _retryDecryption. If - * requestKeysOnFail is true, we'll send an m.room_key_request when we fail - * to decrypt the event due to missing megolm keys. - * - * @param event the event - */ - private fun requestKeysForEvent(event: Event) { - outgoingKeyRequestManager.requestKeyForEvent(event, false) - } - - /** - * Handle a key event. - * - * @param event the key event. - * @param defaultKeysBackupService the keys backup service - * @param forceAccept if true will force to accept the forwarded key - */ - override suspend fun onRoomKeyEvent(event: Event, defaultKeysBackupService: DefaultKeysBackupService, forceAccept: Boolean) { - Timber.tag(loggerTag.value).v("onRoomKeyEvent(${event.getSenderKey()})") - var exportFormat = false - val roomKeyContent = event.getDecryptedContent()?.toModel() ?: return - - val eventSenderKey: String = event.getSenderKey() ?: return Unit.also { - Timber.tag(loggerTag.value).e("onRoom Key/Forward Event() : event is missing sender_key field") - } - - // this device might not been downloaded now? - val fromDevice = cryptoStore.deviceWithIdentityKey(eventSenderKey) - - lateinit var sessionInitiatorSenderKey: String - val trusted: Boolean - - var keysClaimed: MutableMap = HashMap() - val forwardingCurve25519KeyChain: MutableList = ArrayList() - - if (roomKeyContent.roomId.isNullOrEmpty() || roomKeyContent.sessionId.isNullOrEmpty() || roomKeyContent.sessionKey.isNullOrEmpty()) { - Timber.tag(loggerTag.value).e("onRoomKeyEvent() : Key event is missing fields") - return - } - if (event.getDecryptedType() == EventType.FORWARDED_ROOM_KEY) { - if (!cryptoStore.isKeyGossipingEnabled()) { - Timber.tag(loggerTag.value) - .i("onRoomKeyEvent(), ignore forward adding as per crypto config : ${roomKeyContent.roomId}|${roomKeyContent.sessionId}") - return - } - Timber.tag(loggerTag.value).i("onRoomKeyEvent(), forward adding key : ${roomKeyContent.roomId}|${roomKeyContent.sessionId}") - val forwardedRoomKeyContent = event.getDecryptedContent()?.toModel() - ?: return - - forwardedRoomKeyContent.forwardingCurve25519KeyChain?.let { - forwardingCurve25519KeyChain.addAll(it) - } - - forwardingCurve25519KeyChain.add(eventSenderKey) - - exportFormat = true - sessionInitiatorSenderKey = forwardedRoomKeyContent.senderKey ?: return Unit.also { - Timber.tag(loggerTag.value).e("onRoomKeyEvent() : forwarded_room_key event is missing sender_key field") - } - - if (null == forwardedRoomKeyContent.senderClaimedEd25519Key) { - Timber.tag(loggerTag.value).e("forwarded_room_key_event is missing sender_claimed_ed25519_key field") - return - } - - keysClaimed["ed25519"] = forwardedRoomKeyContent.senderClaimedEd25519Key - - // checking if was requested once. - // should we check if the request is sort of active? - val wasNotRequested = cryptoStore.getOutgoingRoomKeyRequest( - roomId = forwardedRoomKeyContent.roomId.orEmpty(), - sessionId = forwardedRoomKeyContent.sessionId.orEmpty(), - algorithm = forwardedRoomKeyContent.algorithm.orEmpty(), - senderKey = forwardedRoomKeyContent.senderKey.orEmpty(), - ).isEmpty() - - trusted = false - - if (!forceAccept && wasNotRequested) { -// val senderId = cryptoStore.deviceWithIdentityKey(event.getSenderKey().orEmpty())?.userId.orEmpty() - unrequestedForwardManager.onUnRequestedKeyForward(roomKeyContent.roomId, event, clock.epochMillis()) - // Ignore unsolicited - Timber.tag(loggerTag.value).w("Ignoring forwarded_room_key_event for ${roomKeyContent.sessionId} that was not requested") - return - } - - // Check who sent the request, as we requested we have the device keys (no need to download) - val sessionThatIsSharing = cryptoStore.deviceWithIdentityKey(eventSenderKey) - if (sessionThatIsSharing == null) { - Timber.tag(loggerTag.value).w("Ignoring forwarded_room_key from unknown device with identity $eventSenderKey") - return - } - val isOwnDevice = myUserId == sessionThatIsSharing.userId - val isDeviceVerified = sessionThatIsSharing.isVerified - val isFromSessionInitiator = sessionThatIsSharing.identityKey() == sessionInitiatorSenderKey - - val isLegitForward = (isOwnDevice && isDeviceVerified) || - (!cryptoConfig.limitRoomKeyRequestsToMyDevices && isFromSessionInitiator) - - val shouldAcceptForward = forceAccept || isLegitForward - - if (!shouldAcceptForward) { - Timber.tag(loggerTag.value) - .w("Ignoring forwarded_room_key device:$eventSenderKey, ownVerified:{$isOwnDevice&&$isDeviceVerified}," + - " fromInitiator:$isFromSessionInitiator") - return - } - } else { - // It's a m.room_key so safe - trusted = true - sessionInitiatorSenderKey = eventSenderKey - Timber.tag(loggerTag.value).i("onRoomKeyEvent(), Adding key : ${roomKeyContent.roomId}|${roomKeyContent.sessionId}") - // inherit the claimed ed25519 key from the setup message - keysClaimed = event.getKeysClaimed().toMutableMap() - } - - Timber.tag(loggerTag.value).i("onRoomKeyEvent addInboundGroupSession ${roomKeyContent.sessionId}") - val addSessionResult = olmDevice.addInboundGroupSession( - sessionId = roomKeyContent.sessionId, - sessionKey = roomKeyContent.sessionKey, - roomId = roomKeyContent.roomId, - senderKey = sessionInitiatorSenderKey, - forwardingCurve25519KeyChain = forwardingCurve25519KeyChain, - keysClaimed = keysClaimed, - exportFormat = exportFormat, - sharedHistory = roomKeyContent.getSharedKey(), - trusted = trusted - ).also { - Timber.tag(loggerTag.value).v(".. onRoomKeyEvent addInboundGroupSession ${roomKeyContent.sessionId} result: $it") - } - - when (addSessionResult) { - is MXOlmDevice.AddSessionResult.Imported -> addSessionResult.ratchetIndex - is MXOlmDevice.AddSessionResult.NotImportedHigherIndex -> addSessionResult.newIndex - else -> null - }?.let { index -> - if (event.getClearType() == EventType.FORWARDED_ROOM_KEY) { - outgoingKeyRequestManager.onRoomKeyForwarded( - sessionId = roomKeyContent.sessionId, - algorithm = roomKeyContent.algorithm ?: "", - roomId = roomKeyContent.roomId, - senderKey = sessionInitiatorSenderKey, - fromIndex = index, - fromDevice = fromDevice?.deviceId, - event = event - ) - - cryptoStore.saveIncomingForwardKeyAuditTrail( - roomId = roomKeyContent.roomId, - sessionId = roomKeyContent.sessionId, - senderKey = sessionInitiatorSenderKey, - algorithm = roomKeyContent.algorithm ?: "", - userId = event.senderId.orEmpty(), - deviceId = fromDevice?.deviceId.orEmpty(), - chainIndex = index.toLong() - ) - - // The index is used to decide if we cancel sent request or if we wait for a better key - outgoingKeyRequestManager.postCancelRequestForSessionIfNeeded(roomKeyContent.sessionId, roomKeyContent.roomId, sessionInitiatorSenderKey, index) - } - } - - if (addSessionResult is MXOlmDevice.AddSessionResult.Imported) { - Timber.tag(loggerTag.value) - .d("onRoomKeyEvent(${event.getClearType()}) : Added megolm session ${roomKeyContent.sessionId} in ${roomKeyContent.roomId}") - defaultKeysBackupService.maybeBackupKeys() - - onNewSession(roomKeyContent.roomId, sessionInitiatorSenderKey, roomKeyContent.sessionId) - } - } - - /** - * Returns boolean shared key flag, if enabled with respect to matrix configuration. - */ - private fun RoomKeyContent.getSharedKey(): Boolean { - if (!cryptoStore.isShareKeysOnInviteEnabled()) return false - return sharedHistory ?: false - } - - /** - * Check if the some messages can be decrypted with a new session. - * - * @param roomId the room id where the new Megolm session has been created for, may be null when importing from external sessions - * @param senderKey the session sender key - * @param sessionId the session id - */ - fun onNewSession(roomId: String?, senderKey: String, sessionId: String) { - Timber.tag(loggerTag.value).v("ON NEW SESSION $sessionId - $senderKey") - newSessionListener?.onNewSession(roomId, sessionId) - } -} diff --git a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmDecryptionFactory.kt b/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmDecryptionFactory.kt deleted file mode 100644 index d8743372ad..0000000000 --- a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmDecryptionFactory.kt +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright (c) 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. - */ - -package org.matrix.android.sdk.internal.crypto.algorithms.megolm - -import dagger.Lazy -import org.matrix.android.sdk.api.crypto.MXCryptoConfig -import org.matrix.android.sdk.internal.crypto.MXOlmDevice -import org.matrix.android.sdk.internal.crypto.OutgoingKeyRequestManager -import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore -import org.matrix.android.sdk.internal.di.UserId -import org.matrix.android.sdk.internal.session.StreamEventsManager -import org.matrix.android.sdk.internal.util.time.Clock -import javax.inject.Inject - -internal class MXMegolmDecryptionFactory @Inject constructor( - private val olmDevice: MXOlmDevice, - @UserId private val myUserId: String, - private val outgoingKeyRequestManager: OutgoingKeyRequestManager, - private val cryptoStore: IMXCryptoStore, - private val eventsManager: Lazy, - private val unrequestedForwardManager: UnRequestedForwardManager, - private val mxCryptoConfig: MXCryptoConfig, - private val clock: Clock, -) { - - fun create(): MXMegolmDecryption { - return MXMegolmDecryption( - olmDevice = olmDevice, - myUserId = myUserId, - outgoingKeyRequestManager = outgoingKeyRequestManager, - cryptoStore = cryptoStore, - liveEventManager = eventsManager, - unrequestedForwardManager = unrequestedForwardManager, - cryptoConfig = mxCryptoConfig, - clock = clock, - ) - } -} diff --git a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmEncryption.kt b/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmEncryption.kt deleted file mode 100644 index 662e1435d3..0000000000 --- a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmEncryption.kt +++ /dev/null @@ -1,613 +0,0 @@ -/* - * Copyright 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. - */ - -package org.matrix.android.sdk.internal.crypto.algorithms.megolm - -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.launch -import kotlinx.coroutines.sync.withLock -import kotlinx.coroutines.withContext -import org.matrix.android.sdk.api.MatrixCoroutineDispatchers -import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_MEGOLM -import org.matrix.android.sdk.api.logger.LoggerTag -import org.matrix.android.sdk.api.session.crypto.MXCryptoError -import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo -import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap -import org.matrix.android.sdk.api.session.crypto.model.forEach -import org.matrix.android.sdk.api.session.events.model.Content -import org.matrix.android.sdk.api.session.events.model.EventType -import org.matrix.android.sdk.api.session.events.model.content.RoomKeyWithHeldContent -import org.matrix.android.sdk.api.session.events.model.content.WithHeldCode -import org.matrix.android.sdk.api.session.room.model.message.MessageContent -import org.matrix.android.sdk.api.session.room.model.message.MessageType -import org.matrix.android.sdk.internal.crypto.DeviceListManager -import org.matrix.android.sdk.internal.crypto.InboundGroupSessionHolder -import org.matrix.android.sdk.internal.crypto.MXOlmDevice -import org.matrix.android.sdk.internal.crypto.actions.EnsureOlmSessionsForDevicesAction -import org.matrix.android.sdk.internal.crypto.actions.MessageEncrypter -import org.matrix.android.sdk.internal.crypto.algorithms.IMXEncrypting -import org.matrix.android.sdk.internal.crypto.algorithms.IMXGroupEncryption -import org.matrix.android.sdk.internal.crypto.keysbackup.DefaultKeysBackupService -import org.matrix.android.sdk.internal.crypto.model.toDebugCount -import org.matrix.android.sdk.internal.crypto.model.toDebugString -import org.matrix.android.sdk.internal.crypto.repository.WarnOnUnknownDeviceRepository -import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore -import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask -import org.matrix.android.sdk.internal.util.JsonCanonicalizer -import org.matrix.android.sdk.internal.util.convertToUTF8 -import org.matrix.android.sdk.internal.util.time.Clock -import timber.log.Timber - -private val loggerTag = LoggerTag("MXMegolmEncryption", LoggerTag.CRYPTO) - -internal class MXMegolmEncryption( - // The id of the room we will be sending to. - private val roomId: String, - private val olmDevice: MXOlmDevice, - private val defaultKeysBackupService: DefaultKeysBackupService, - private val cryptoStore: IMXCryptoStore, - private val deviceListManager: DeviceListManager, - private val ensureOlmSessionsForDevicesAction: EnsureOlmSessionsForDevicesAction, - private val myUserId: String, - private val myDeviceId: String, - private val sendToDeviceTask: SendToDeviceTask, - private val messageEncrypter: MessageEncrypter, - private val warnOnUnknownDevicesRepository: WarnOnUnknownDeviceRepository, - private val coroutineDispatchers: MatrixCoroutineDispatchers, - private val cryptoCoroutineScope: CoroutineScope, - private val clock: Clock, -) : IMXEncrypting, IMXGroupEncryption { - - // OutboundSessionInfo. Null if we haven't yet started setting one up. Note - // that even if this is non-null, it may not be ready for use (in which - // case outboundSession.shareOperation will be non-null.) - private var outboundSession: MXOutboundSessionInfo? = null - - init { - // restore existing outbound session if any - outboundSession = olmDevice.restoreOutboundGroupSessionForRoom(roomId) - } - - // Default rotation periods - // TODO Make it configurable via parameters - // Session rotation periods - private var sessionRotationPeriodMsgs: Int = 100 - private var sessionRotationPeriodMs: Int = 7 * 24 * 3600 * 1000 - - override suspend fun encryptEventContent( - eventContent: Content, - eventType: String, - userIds: List - ): Content { - val ts = clock.epochMillis() - Timber.tag(loggerTag.value).v("encryptEventContent : getDevicesInRoom") - - /** - * When using in-room messages and the room has encryption enabled, - * clients should ensure that encryption does not hinder the verification. - * For example, if the verification messages are encrypted, clients must ensure that all the recipient’s - * unverified devices receive the keys necessary to decrypt the messages, - * even if they would normally not be given the keys to decrypt messages in the room. - */ - val shouldSendToUnverified = isVerificationEvent(eventType, eventContent) - - val devices = getDevicesInRoom(userIds, forceDistributeToUnverified = shouldSendToUnverified) - - Timber.tag(loggerTag.value).d("encrypt event in room=$roomId - devices count in room ${devices.allowedDevices.toDebugCount()}") - Timber.tag(loggerTag.value).v("encryptEventContent ${clock.epochMillis() - ts}: getDevicesInRoom ${devices.allowedDevices.toDebugString()}") - val outboundSession = ensureOutboundSession(devices.allowedDevices) - - return encryptContent(outboundSession, eventType, eventContent) - .also { - notifyWithheldForSession(devices.withHeldDevices, outboundSession) - // annoyingly we have to serialize again the saved outbound session to store message index :/ - // if not we would see duplicate message index errors - olmDevice.storeOutboundGroupSessionForRoom(roomId, outboundSession.sessionId) - Timber.tag(loggerTag.value).d("encrypt event in room=$roomId Finished in ${clock.epochMillis() - ts} millis") - } - } - - private fun isVerificationEvent(eventType: String, eventContent: Content) = - EventType.isVerificationEvent(eventType) || - (eventType == EventType.MESSAGE && - eventContent.get(MessageContent.MSG_TYPE_JSON_KEY) == MessageType.MSGTYPE_VERIFICATION_REQUEST) - - private fun notifyWithheldForSession(devices: MXUsersDevicesMap, outboundSession: MXOutboundSessionInfo) { - // offload to computation thread - cryptoCoroutineScope.launch(coroutineDispatchers.computation) { - mutableListOf>().apply { - devices.forEach { userId, deviceId, withheldCode -> - this.add(UserDevice(userId, deviceId) to withheldCode) - } - }.groupBy( - { it.second }, - { it.first } - ).forEach { (code, targets) -> - notifyKeyWithHeld(targets, outboundSession.sessionId, olmDevice.deviceCurve25519Key, code) - } - } - } - - override fun discardSessionKey() { - outboundSession = null - olmDevice.discardOutboundGroupSessionForRoom(roomId) - } - - override suspend fun preshareKey(userIds: List) { - val ts = clock.epochMillis() - Timber.tag(loggerTag.value).d("preshareKey started in $roomId ...") - val devices = getDevicesInRoom(userIds) - val outboundSession = ensureOutboundSession(devices.allowedDevices) - - notifyWithheldForSession(devices.withHeldDevices, outboundSession) - - Timber.tag(loggerTag.value).d("preshareKey in $roomId done in ${clock.epochMillis() - ts} millis") - } - - /** - * Prepare a new session. - * - * @return the session description - */ - private fun prepareNewSessionInRoom(): MXOutboundSessionInfo { - Timber.tag(loggerTag.value).v("prepareNewSessionInRoom() ") - val sessionId = olmDevice.createOutboundGroupSessionForRoom(roomId) - - val keysClaimedMap = mapOf( - "ed25519" to olmDevice.deviceEd25519Key!! - ) - - val sharedHistory = cryptoStore.shouldShareHistory(roomId) - Timber.tag(loggerTag.value).v("prepareNewSessionInRoom() as sharedHistory $sharedHistory") - olmDevice.addInboundGroupSession( - sessionId = sessionId!!, - sessionKey = olmDevice.getSessionKey(sessionId)!!, - roomId = roomId, - senderKey = olmDevice.deviceCurve25519Key!!, - forwardingCurve25519KeyChain = emptyList(), - keysClaimed = keysClaimedMap, - exportFormat = false, - sharedHistory = sharedHistory, - trusted = true - ) - - cryptoCoroutineScope.launch { - defaultKeysBackupService.maybeBackupKeys() - } - - return MXOutboundSessionInfo( - sessionId = sessionId, - sharedWithHelper = SharedWithHelper(roomId, sessionId, cryptoStore), - clock = clock, - sharedHistory = sharedHistory - ) - } - - /** - * Ensure the outbound session. - * - * @param devicesInRoom the devices list - */ - private suspend fun ensureOutboundSession(devicesInRoom: MXUsersDevicesMap): MXOutboundSessionInfo { - Timber.tag(loggerTag.value).v("ensureOutboundSession roomId:$roomId") - var session = outboundSession - if (session == null || - // Need to make a brand new session? - session.needsRotation(sessionRotationPeriodMsgs, sessionRotationPeriodMs) || - // Is there a room history visibility change since the last outboundSession - cryptoStore.shouldShareHistory(roomId) != session.sharedHistory || - // Determine if we have shared with anyone we shouldn't have - session.sharedWithTooManyDevices(devicesInRoom)) { - Timber.tag(loggerTag.value).d("roomId:$roomId Starting new megolm session because we need to rotate.") - session = prepareNewSessionInRoom() - outboundSession = session - } - val safeSession = session - val shareMap = HashMap>()/* userId */ - val userIds = devicesInRoom.userIds - for (userId in userIds) { - val deviceIds = devicesInRoom.getUserDeviceIds(userId) - for (deviceId in deviceIds!!) { - val deviceInfo = devicesInRoom.getObject(userId, deviceId) - if (deviceInfo != null && !cryptoStore.getSharedSessionInfo(roomId, safeSession.sessionId, deviceInfo).found) { - val devices = shareMap.getOrPut(userId) { ArrayList() } - devices.add(deviceInfo) - } - } - } - val devicesCount = shareMap.entries.fold(0) { acc, new -> acc + new.value.size } - Timber.tag(loggerTag.value).d("roomId:$roomId found $devicesCount devices without megolm session(${session.sessionId})") - shareKey(safeSession, shareMap) - return safeSession - } - - /** - * Share the device key to a list of users. - * - * @param session the session info - * @param devicesByUsers the devices map - */ - private suspend fun shareKey( - session: MXOutboundSessionInfo, - devicesByUsers: Map> - ) { - // nothing to send, the task is done - if (devicesByUsers.isEmpty()) { - Timber.tag(loggerTag.value).v("shareKey() : nothing more to do") - return - } - // reduce the map size to avoid request timeout when there are too many devices (Users size * devices per user) - val subMap = HashMap>() - var devicesCount = 0 - for ((userId, devices) in devicesByUsers) { - subMap[userId] = devices - devicesCount += devices.size - if (devicesCount > 100) { - break - } - } - Timber.tag(loggerTag.value).v("shareKey() ; sessionId<${session.sessionId}> userId ${subMap.keys}") - shareUserDevicesKey(session, subMap) - val remainingDevices = devicesByUsers - subMap.keys - shareKey(session, remainingDevices) - } - - /** - * Share the device keys of a an user. - * - * @param sessionInfo the session info - * @param devicesByUser the devices map - */ - private suspend fun shareUserDevicesKey( - sessionInfo: MXOutboundSessionInfo, - devicesByUser: Map> - ) { - val sessionKey = olmDevice.getSessionKey(sessionInfo.sessionId) ?: return Unit.also { - Timber.tag(loggerTag.value).v("shareUserDevicesKey() Failed to share session, failed to export") - } - val chainIndex = olmDevice.getMessageIndex(sessionInfo.sessionId) - - val payload = mapOf( - "type" to EventType.ROOM_KEY, - "content" to mapOf( - "algorithm" to MXCRYPTO_ALGORITHM_MEGOLM, - "room_id" to roomId, - "session_id" to sessionInfo.sessionId, - "session_key" to sessionKey, - "chain_index" to chainIndex, - "org.matrix.msc3061.shared_history" to sessionInfo.sharedHistory - ) - ) - - var t0 = clock.epochMillis() - Timber.tag(loggerTag.value).v("shareUserDevicesKey() : starts") - - val results = ensureOlmSessionsForDevicesAction.handle(devicesByUser) - Timber.tag(loggerTag.value).v( - """shareUserDevicesKey(): ensureOlmSessionsForDevices succeeds after ${clock.epochMillis() - t0} ms""" - .trimMargin() - ) - val contentMap = MXUsersDevicesMap() - var haveTargets = false - val userIds = results.userIds - val noOlmToNotify = mutableListOf() - for (userId in userIds) { - val devicesToShareWith = devicesByUser[userId] - for ((deviceID) in devicesToShareWith!!) { - val sessionResult = results.getObject(userId, deviceID) - if (sessionResult?.sessionId == null) { - // no session with this device, probably because there - // were no one-time keys. - - // MSC 2399 - // send withheld m.no_olm: an olm session could not be established. - // This may happen, for example, if the sender was unable to obtain a one-time key from the recipient. - Timber.tag(loggerTag.value).v("shareUserDevicesKey() : No Olm Session for $userId:$deviceID mark for withheld") - noOlmToNotify.add(UserDevice(userId, deviceID)) - continue - } - Timber.tag(loggerTag.value).v("shareUserDevicesKey() : Add to share keys contentMap for $userId:$deviceID") - contentMap.setObject(userId, deviceID, messageEncrypter.encryptMessage(payload, listOf(sessionResult.deviceInfo))) - haveTargets = true - } - } - - // Add the devices we have shared with to session.sharedWithDevices. - // we deliberately iterate over devicesByUser (ie, the devices we - // attempted to share with) rather than the contentMap (those we did - // share with), because we don't want to try to claim a one-time-key - // for dead devices on every message. - for ((_, devicesToShareWith) in devicesByUser) { - for (deviceInfo in devicesToShareWith) { - sessionInfo.sharedWithHelper.markedSessionAsShared(deviceInfo, chainIndex) - // XXX is it needed to add it to the audit trail? - // For now decided that no, we are more interested by forward trail - } - } - - if (haveTargets) { - t0 = clock.epochMillis() - Timber.tag(loggerTag.value).i("shareUserDevicesKey() ${sessionInfo.sessionId} : has target") - Timber.tag(loggerTag.value).d("sending to device room key for ${sessionInfo.sessionId} to ${contentMap.toDebugString()}") - val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, contentMap) - try { - withContext(coroutineDispatchers.io) { - sendToDeviceTask.execute(sendToDeviceParams) - } - Timber.tag(loggerTag.value).i("shareUserDevicesKey() : sendToDevice succeeds after ${clock.epochMillis() - t0} ms") - } catch (failure: Throwable) { - // What to do here... - Timber.tag(loggerTag.value).e("shareUserDevicesKey() : Failed to share <${sessionInfo.sessionId}>") - } - } else { - Timber.tag(loggerTag.value).i("shareUserDevicesKey() : no need to share key") - } - - if (noOlmToNotify.isNotEmpty()) { - // XXX offload?, as they won't read the message anyhow? - notifyKeyWithHeld( - noOlmToNotify, - sessionInfo.sessionId, - olmDevice.deviceCurve25519Key, - WithHeldCode.NO_OLM - ) - } - } - - private suspend fun notifyKeyWithHeld( - targets: List, - sessionId: String, - senderKey: String?, - code: WithHeldCode - ) { - Timber.tag(loggerTag.value).d( - "notifyKeyWithHeld() :sending withheld for session:$sessionId and code $code to" + - " ${targets.joinToString { "${it.userId}|${it.deviceId}" }}" - ) - val withHeldContent = RoomKeyWithHeldContent( - roomId = roomId, - senderKey = senderKey, - algorithm = MXCRYPTO_ALGORITHM_MEGOLM, - sessionId = sessionId, - codeString = code.value, - fromDevice = myDeviceId - ) - val params = SendToDeviceTask.Params( - EventType.ROOM_KEY_WITHHELD.stable, - MXUsersDevicesMap().apply { - targets.forEach { - setObject(it.userId, it.deviceId, withHeldContent) - } - } - ) - try { - withContext(coroutineDispatchers.io) { - sendToDeviceTask.execute(params) - } - } catch (failure: Throwable) { - Timber.tag(loggerTag.value) - .e("notifyKeyWithHeld() :$sessionId Failed to send withheld ${targets.map { "${it.userId}|${it.deviceId}" }}") - } - } - - /** - * process the pending encryptions. - */ - private fun encryptContent(session: MXOutboundSessionInfo, eventType: String, eventContent: Content): Content { - // Everything is in place, encrypt all pending events - val payloadJson = HashMap() - payloadJson["room_id"] = roomId - payloadJson["type"] = eventType - payloadJson["content"] = eventContent - - // Get canonical Json from - - val payloadString = convertToUTF8(JsonCanonicalizer.getCanonicalJson(Map::class.java, payloadJson)) - val ciphertext = olmDevice.encryptGroupMessage(session.sessionId, payloadString) - - val map = HashMap() - map["algorithm"] = MXCRYPTO_ALGORITHM_MEGOLM - map["sender_key"] = olmDevice.deviceCurve25519Key!! - map["ciphertext"] = ciphertext!! - map["session_id"] = session.sessionId - - // Include our device ID so that recipients can send us a - // m.new_device message if they don't have our session key. - map["device_id"] = myDeviceId - session.useCount++ - return map - } - - /** - * Get the list of devices which can encrypt data to. - * This method must be called in getDecryptingThreadHandler() thread. - * - * @param userIds the user ids whose devices must be checked. - * @param forceDistributeToUnverified If true the unverified devices will be included in valid recipients even if - * such devices are blocked in crypto settings - */ - private suspend fun getDevicesInRoom(userIds: List, forceDistributeToUnverified: Boolean = false): DeviceInRoomInfo { - // We are happy to use a cached version here: we assume that if we already - // have a list of the user's devices, then we already share an e2e room - // with them, which means that they will have announced any new devices via - // an m.new_device. - val keys = deviceListManager.downloadKeys(userIds, false) - val encryptToVerifiedDevicesOnly = cryptoStore.getGlobalBlacklistUnverifiedDevices() || - cryptoStore.getBlockUnverifiedDevices(roomId) - - val devicesInRoom = DeviceInRoomInfo() - val unknownDevices = MXUsersDevicesMap() - - for (userId in keys.userIds) { - val deviceIds = keys.getUserDeviceIds(userId) ?: continue - for (deviceId in deviceIds) { - val deviceInfo = keys.getObject(userId, deviceId) ?: continue - if (warnOnUnknownDevicesRepository.warnOnUnknownDevices() && deviceInfo.isUnknown) { - // The device is not yet known by the user - unknownDevices.setObject(userId, deviceId, deviceInfo) - continue - } - if (deviceInfo.isBlocked) { - // Remove any blocked devices - devicesInRoom.withHeldDevices.setObject(userId, deviceId, WithHeldCode.BLACKLISTED) - continue - } - - if (!deviceInfo.isVerified && encryptToVerifiedDevicesOnly && !forceDistributeToUnverified) { - devicesInRoom.withHeldDevices.setObject(userId, deviceId, WithHeldCode.UNVERIFIED) - continue - } - - if (deviceInfo.identityKey() == olmDevice.deviceCurve25519Key) { - // Don't bother sending to ourself - continue - } - devicesInRoom.allowedDevices.setObject(userId, deviceId, deviceInfo) - } - } - if (unknownDevices.isEmpty) { - return devicesInRoom - } else { - throw MXCryptoError.UnknownDevice(unknownDevices) - } - } - - override suspend fun reshareKey( - groupSessionId: String, - userId: String, - deviceId: String, - senderKey: String - ): Boolean { - Timber.tag(loggerTag.value).i("process reshareKey for $groupSessionId to $userId:$deviceId") - val deviceInfo = cryptoStore.getUserDevice(userId, deviceId) ?: return false - .also { Timber.tag(loggerTag.value).w("reshareKey: Device not found") } - - // Get the chain index of the key we previously sent this device - val wasSessionSharedWithUser = cryptoStore.getSharedSessionInfo(roomId, groupSessionId, deviceInfo) - if (!wasSessionSharedWithUser.found) { - // This session was never shared with this user - // Send a room key with held - notifyKeyWithHeld(listOf(UserDevice(userId, deviceId)), groupSessionId, senderKey, WithHeldCode.UNAUTHORISED) - Timber.tag(loggerTag.value).w("reshareKey: ERROR : Never shared megolm with this device") - return false - } - // if found chain index should not be null - val chainIndex = wasSessionSharedWithUser.chainIndex ?: return false - .also { - Timber.tag(loggerTag.value).w("reshareKey: Null chain index") - } - - val devicesByUser = mapOf(userId to listOf(deviceInfo)) - val usersDeviceMap = try { - ensureOlmSessionsForDevicesAction.handle(devicesByUser) - } catch (failure: Throwable) { - null - } - val olmSessionResult = usersDeviceMap?.getObject(userId, deviceId) - if (olmSessionResult?.sessionId == null) { - Timber.tag(loggerTag.value).w("reshareKey: no session with this device, probably because there were no one-time keys") - return false - } - Timber.tag(loggerTag.value).i(" reshareKey: $groupSessionId:$chainIndex with device $userId:$deviceId using session ${olmSessionResult.sessionId}") - - val sessionHolder = try { - olmDevice.getInboundGroupSession(groupSessionId, senderKey, roomId) - } catch (failure: Throwable) { - Timber.tag(loggerTag.value).e(failure, "shareKeysWithDevice: failed to get session $groupSessionId") - return false - } - - val export = sessionHolder.mutex.withLock { - sessionHolder.wrapper.exportKeys() - } ?: return false.also { - Timber.tag(loggerTag.value).e("shareKeysWithDevice: failed to export group session $groupSessionId") - } - - val payloadJson = mapOf( - "type" to EventType.FORWARDED_ROOM_KEY, - "content" to export - ) - - val encodedPayload = messageEncrypter.encryptMessage(payloadJson, listOf(deviceInfo)) - val sendToDeviceMap = MXUsersDevicesMap() - sendToDeviceMap.setObject(userId, deviceId, encodedPayload) - Timber.tag(loggerTag.value).i("reshareKey() : sending session $groupSessionId to $userId:$deviceId") - val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, sendToDeviceMap) - return try { - sendToDeviceTask.execute(sendToDeviceParams) - Timber.tag(loggerTag.value).i("reshareKey() : successfully send <$groupSessionId> to $userId:$deviceId") - true - } catch (failure: Throwable) { - Timber.tag(loggerTag.value).e(failure, "reshareKey() : fail to send <$groupSessionId> to $userId:$deviceId") - false - } - } - - @Throws - override suspend fun shareHistoryKeysWithDevice(inboundSessionWrapper: InboundGroupSessionHolder, deviceInfo: CryptoDeviceInfo) { - require(inboundSessionWrapper.wrapper.sessionData.sharedHistory) { "This key can't be shared" } - Timber.tag(loggerTag.value).i("process shareHistoryKeys for ${inboundSessionWrapper.wrapper.safeSessionId} to ${deviceInfo.shortDebugString()}") - val userId = deviceInfo.userId - val deviceId = deviceInfo.deviceId - val devicesByUser = mapOf(userId to listOf(deviceInfo)) - val usersDeviceMap = try { - ensureOlmSessionsForDevicesAction.handle(devicesByUser) - } catch (failure: Throwable) { - Timber.tag(loggerTag.value).i(failure, "process shareHistoryKeys failed to ensure olm") - // process anyway? - null - } - val olmSessionResult = usersDeviceMap?.getObject(userId, deviceId) - if (olmSessionResult?.sessionId == null) { - Timber.tag(loggerTag.value).w("shareHistoryKeys: no session with this device, probably because there were no one-time keys") - return - } - - val export = inboundSessionWrapper.mutex.withLock { - inboundSessionWrapper.wrapper.exportKeys() - } ?: return Unit.also { - Timber.tag(loggerTag.value).e("shareHistoryKeys: failed to export group session ${inboundSessionWrapper.wrapper.safeSessionId}") - } - - val payloadJson = mapOf( - "type" to EventType.FORWARDED_ROOM_KEY, - "content" to export - ) - - val encodedPayload = - withContext(coroutineDispatchers.computation) { - messageEncrypter.encryptMessage(payloadJson, listOf(deviceInfo)) - } - val sendToDeviceMap = MXUsersDevicesMap() - sendToDeviceMap.setObject(userId, deviceId, encodedPayload) - Timber.tag(loggerTag.value) - .d("shareHistoryKeys() : sending session ${inboundSessionWrapper.wrapper.safeSessionId} to ${deviceInfo.shortDebugString()}") - val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, sendToDeviceMap) - withContext(coroutineDispatchers.io) { - sendToDeviceTask.execute(sendToDeviceParams) - } - } - - data class DeviceInRoomInfo( - val allowedDevices: MXUsersDevicesMap = MXUsersDevicesMap(), - val withHeldDevices: MXUsersDevicesMap = MXUsersDevicesMap() - ) - - data class UserDevice( - val userId: String, - val deviceId: String - ) -} diff --git a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmEncryptionFactory.kt b/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmEncryptionFactory.kt deleted file mode 100644 index 4225d604aa..0000000000 --- a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmEncryptionFactory.kt +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Copyright 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. - */ - -package org.matrix.android.sdk.internal.crypto.algorithms.megolm - -import kotlinx.coroutines.CoroutineScope -import org.matrix.android.sdk.api.MatrixCoroutineDispatchers -import org.matrix.android.sdk.internal.crypto.DeviceListManager -import org.matrix.android.sdk.internal.crypto.MXOlmDevice -import org.matrix.android.sdk.internal.crypto.actions.EnsureOlmSessionsForDevicesAction -import org.matrix.android.sdk.internal.crypto.actions.MessageEncrypter -import org.matrix.android.sdk.internal.crypto.keysbackup.DefaultKeysBackupService -import org.matrix.android.sdk.internal.crypto.repository.WarnOnUnknownDeviceRepository -import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore -import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask -import org.matrix.android.sdk.internal.di.DeviceId -import org.matrix.android.sdk.internal.di.UserId -import org.matrix.android.sdk.internal.util.time.Clock -import javax.inject.Inject - -internal class MXMegolmEncryptionFactory @Inject constructor( - private val olmDevice: MXOlmDevice, - private val defaultKeysBackupService: DefaultKeysBackupService, - private val cryptoStore: IMXCryptoStore, - private val deviceListManager: DeviceListManager, - private val ensureOlmSessionsForDevicesAction: EnsureOlmSessionsForDevicesAction, - @UserId private val userId: String, - @DeviceId private val deviceId: String?, - private val sendToDeviceTask: SendToDeviceTask, - private val messageEncrypter: MessageEncrypter, - private val warnOnUnknownDevicesRepository: WarnOnUnknownDeviceRepository, - private val coroutineDispatchers: MatrixCoroutineDispatchers, - private val cryptoCoroutineScope: CoroutineScope, - private val clock: Clock, -) { - - fun create(roomId: String): MXMegolmEncryption { - return MXMegolmEncryption( - roomId = roomId, - olmDevice = olmDevice, - defaultKeysBackupService = defaultKeysBackupService, - cryptoStore = cryptoStore, - deviceListManager = deviceListManager, - ensureOlmSessionsForDevicesAction = ensureOlmSessionsForDevicesAction, - myUserId = userId, - myDeviceId = deviceId!!, - sendToDeviceTask = sendToDeviceTask, - messageEncrypter = messageEncrypter, - warnOnUnknownDevicesRepository = warnOnUnknownDevicesRepository, - coroutineDispatchers = coroutineDispatchers, - cryptoCoroutineScope = cryptoCoroutineScope, - clock = clock, - ) - } -} diff --git a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXOutboundSessionInfo.kt b/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXOutboundSessionInfo.kt deleted file mode 100644 index e0caa0d9a5..0000000000 --- a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXOutboundSessionInfo.kt +++ /dev/null @@ -1,77 +0,0 @@ -/* - * Copyright 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. - */ - -package org.matrix.android.sdk.internal.crypto.algorithms.megolm - -import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo -import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap -import org.matrix.android.sdk.internal.util.time.Clock -import timber.log.Timber - -internal class MXOutboundSessionInfo( - // The id of the session - val sessionId: String, - val sharedWithHelper: SharedWithHelper, - private val clock: Clock, - // When the session was created - private val creationTime: Long = clock.epochMillis(), - val sharedHistory: Boolean = false -) { - - // Number of times this session has been used - var useCount: Int = 0 - - fun needsRotation(rotationPeriodMsgs: Int, rotationPeriodMs: Int): Boolean { - var needsRotation = false - val sessionLifetime = clock.epochMillis() - creationTime - - if (useCount >= rotationPeriodMsgs || sessionLifetime >= rotationPeriodMs) { - Timber.v("## needsRotation() : Rotating megolm session after $useCount, ${sessionLifetime}ms") - needsRotation = true - } - - return needsRotation - } - - /** - * Determine if this session has been shared with devices which it shouldn't have been. - * - * @param devicesInRoom the devices map - * @return true if we have shared the session with devices which aren't in devicesInRoom. - */ - fun sharedWithTooManyDevices(devicesInRoom: MXUsersDevicesMap): Boolean { - val sharedWithDevices = sharedWithHelper.sharedWithDevices() - val userIds = sharedWithDevices.userIds - - for (userId in userIds) { - if (null == devicesInRoom.getUserDeviceIds(userId)) { - Timber.v("## sharedWithTooManyDevices() : Starting new session because we shared with $userId") - return true - } - - val deviceIds = sharedWithDevices.getUserDeviceIds(userId) - - for (deviceId in deviceIds!!) { - if (null == devicesInRoom.getObject(userId, deviceId)) { - Timber.v("## sharedWithTooManyDevices() : Starting new session because we shared with $userId:$deviceId") - return true - } - } - } - - return false - } -} diff --git a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/SharedWithHelper.kt b/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/SharedWithHelper.kt deleted file mode 100644 index 30fd403ce8..0000000000 --- a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/SharedWithHelper.kt +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright 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. - */ - -package org.matrix.android.sdk.internal.crypto.algorithms.megolm - -import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo -import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap -import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore - -internal class SharedWithHelper( - private val roomId: String, - private val sessionId: String, - private val cryptoStore: IMXCryptoStore -) { - - fun sharedWithDevices(): MXUsersDevicesMap { - return cryptoStore.getSharedWithInfo(roomId, sessionId) - } - - fun markedSessionAsShared(deviceInfo: CryptoDeviceInfo, chainIndex: Int) { - cryptoStore.markedSessionAsShared( - roomId = roomId, - sessionId = sessionId, - userId = deviceInfo.userId, - deviceId = deviceInfo.deviceId, - deviceIdentityKey = deviceInfo.identityKey() ?: "", - chainIndex = chainIndex - ) - } -} diff --git a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/UnRequestedForwardManager.kt b/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/UnRequestedForwardManager.kt deleted file mode 100644 index 9235cd2abf..0000000000 --- a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/UnRequestedForwardManager.kt +++ /dev/null @@ -1,150 +0,0 @@ -/* - * Copyright 2022 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. - */ - -package org.matrix.android.sdk.internal.crypto.algorithms.megolm - -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.SupervisorJob -import kotlinx.coroutines.asCoroutineDispatcher -import kotlinx.coroutines.cancel -import kotlinx.coroutines.launch -import org.matrix.android.sdk.api.extensions.tryOrNull -import org.matrix.android.sdk.api.session.events.model.Event -import org.matrix.android.sdk.internal.crypto.DeviceListManager -import org.matrix.android.sdk.internal.session.SessionScope -import org.matrix.android.sdk.internal.task.SemaphoreCoroutineSequencer -import timber.log.Timber -import java.util.concurrent.Executors -import javax.inject.Inject -import kotlin.math.abs - -private const val INVITE_VALIDITY_TIME_WINDOW_MILLIS = 10 * 60_000 - -@SessionScope -internal class UnRequestedForwardManager @Inject constructor( - private val deviceListManager: DeviceListManager, -) { - - private val dispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher() - private val scope = CoroutineScope(SupervisorJob() + dispatcher) - private val sequencer = SemaphoreCoroutineSequencer() - - // For now only in memory storage. Maybe we should persist? in case of gappy sync and long catchups? - private val forwardedKeysPerRoom = mutableMapOf>>() - - data class InviteInfo( - val roomId: String, - val fromMxId: String, - val timestamp: Long - ) - - data class ForwardInfo( - val event: Event, - val timestamp: Long - ) - - // roomId, local timestamp of invite - private val recentInvites = mutableListOf() - - fun close() { - try { - scope.cancel("User Terminate") - } catch (failure: Throwable) { - Timber.w(failure, "Failed to shutDown UnrequestedForwardManager") - } - } - - fun onInviteReceived(roomId: String, fromUserId: String, localTimeStamp: Long) { - Timber.w("Invite received in room:$roomId from:$fromUserId at $localTimeStamp") - scope.launch { - sequencer.post { - if (!recentInvites.any { it.roomId == roomId && it.fromMxId == fromUserId }) { - recentInvites.add( - InviteInfo( - roomId, - fromUserId, - localTimeStamp - ) - ) - } - } - } - } - - fun onUnRequestedKeyForward(roomId: String, event: Event, localTimeStamp: Long) { - Timber.w("Received unrequested forward in room:$roomId from:${event.senderId} at $localTimeStamp") - scope.launch { - sequencer.post { - val claimSenderId = event.senderId.orEmpty() - val senderKey = event.getSenderKey() - // we might want to download keys, as this user might not be known yet, cache is ok - val ownerMxId = - tryOrNull { - deviceListManager.downloadKeys(listOf(claimSenderId), false) - .map[claimSenderId] - ?.values - ?.firstOrNull { it.identityKey() == senderKey } - ?.userId - } - // Not sure what to do if the device has been deleted? I can't proove the mxid - if (ownerMxId == null || claimSenderId != ownerMxId) { - Timber.w("Mismatch senderId between event and olm owner") - return@post - } - - forwardedKeysPerRoom - .getOrPut(roomId) { mutableMapOf() } - .getOrPut(ownerMxId) { mutableListOf() } - .add(ForwardInfo(event, localTimeStamp)) - } - } - } - - fun postSyncProcessParkedKeysIfNeeded(currentTimestamp: Long, handleForwards: suspend (List) -> Unit) { - scope.launch { - sequencer.post { - // Prune outdated invites - recentInvites.removeAll { currentTimestamp - it.timestamp > INVITE_VALIDITY_TIME_WINDOW_MILLIS } - val cleanUpEvents = mutableListOf>() - forwardedKeysPerRoom.forEach { (roomId, senderIdToForwardMap) -> - senderIdToForwardMap.forEach { (senderId, eventList) -> - // is there a matching invite in a valid timewindow? - val matchingInvite = recentInvites.firstOrNull { it.fromMxId == senderId && it.roomId == roomId } - if (matchingInvite != null) { - Timber.v("match for room:$roomId from sender:$senderId -> count =${eventList.size}") - - eventList.filter { - abs(matchingInvite.timestamp - it.timestamp) <= INVITE_VALIDITY_TIME_WINDOW_MILLIS - }.map { - it.event - }.takeIf { it.isNotEmpty() }?.let { - Timber.w("Re-processing forwarded_room_key_event that was not requested after invite") - scope.launch { - handleForwards.invoke(it) - } - } - cleanUpEvents.add(roomId to senderId) - } - } - } - - cleanUpEvents.forEach { roomIdToSenderPair -> - forwardedKeysPerRoom[roomIdToSenderPair.first]?.get(roomIdToSenderPair.second)?.clear() - } - } - } - } -} diff --git a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/algorithms/olm/MXOlmDecryption.kt b/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/algorithms/olm/MXOlmDecryption.kt deleted file mode 100644 index 4e336abd82..0000000000 --- a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/algorithms/olm/MXOlmDecryption.kt +++ /dev/null @@ -1,269 +0,0 @@ -/* - * 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. - */ - -package org.matrix.android.sdk.internal.crypto.algorithms.olm - -import kotlinx.coroutines.sync.withLock -import org.matrix.android.sdk.api.logger.LoggerTag -import org.matrix.android.sdk.api.session.crypto.MXCryptoError -import org.matrix.android.sdk.api.session.crypto.model.MXEventDecryptionResult -import org.matrix.android.sdk.api.session.events.model.Event -import org.matrix.android.sdk.api.session.events.model.content.OlmEventContent -import org.matrix.android.sdk.api.session.events.model.content.OlmPayloadContent -import org.matrix.android.sdk.api.session.events.model.toModel -import org.matrix.android.sdk.api.util.JSON_DICT_PARAMETERIZED_TYPE -import org.matrix.android.sdk.api.util.JsonDict -import org.matrix.android.sdk.internal.crypto.MXOlmDevice -import org.matrix.android.sdk.internal.crypto.algorithms.IMXDecrypting -import org.matrix.android.sdk.internal.di.MoshiProvider -import org.matrix.android.sdk.internal.util.convertFromUTF8 -import timber.log.Timber - -private val loggerTag = LoggerTag("MXOlmDecryption", LoggerTag.CRYPTO) - -internal class MXOlmDecryption( - // The olm device interface - private val olmDevice: MXOlmDevice, - // the matrix userId - private val userId: String -) : - IMXDecrypting { - - @Throws(MXCryptoError::class) - override suspend fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult { - val olmEventContent = event.content.toModel() ?: run { - Timber.tag(loggerTag.value).e("## decryptEvent() : bad event format") - throw MXCryptoError.Base( - MXCryptoError.ErrorType.BAD_EVENT_FORMAT, - MXCryptoError.BAD_EVENT_FORMAT_TEXT_REASON - ) - } - - val cipherText = olmEventContent.ciphertext ?: run { - Timber.tag(loggerTag.value).e("## decryptEvent() : missing cipher text") - throw MXCryptoError.Base( - MXCryptoError.ErrorType.MISSING_CIPHER_TEXT, - MXCryptoError.MISSING_CIPHER_TEXT_REASON - ) - } - - val senderKey = olmEventContent.senderKey ?: run { - Timber.tag(loggerTag.value).e("## decryptEvent() : missing sender key") - throw MXCryptoError.Base( - MXCryptoError.ErrorType.MISSING_SENDER_KEY, - MXCryptoError.MISSING_SENDER_KEY_TEXT_REASON - ) - } - - val messageAny = cipherText[olmDevice.deviceCurve25519Key] ?: run { - Timber.tag(loggerTag.value).e("## decryptEvent() : our device ${olmDevice.deviceCurve25519Key} is not included in recipients") - throw MXCryptoError.Base(MXCryptoError.ErrorType.NOT_INCLUDE_IN_RECIPIENTS, MXCryptoError.NOT_INCLUDED_IN_RECIPIENT_REASON) - } - - // The message for myUser - @Suppress("UNCHECKED_CAST") - val message = messageAny as JsonDict - - val decryptedPayload = decryptMessage(message, senderKey) - - if (decryptedPayload == null) { - Timber.tag(loggerTag.value).e("## decryptEvent() Failed to decrypt Olm event (id= ${event.eventId} from $senderKey") - throw MXCryptoError.Base(MXCryptoError.ErrorType.BAD_ENCRYPTED_MESSAGE, MXCryptoError.BAD_ENCRYPTED_MESSAGE_REASON) - } - val payloadString = convertFromUTF8(decryptedPayload) - - val adapter = MoshiProvider.providesMoshi().adapter(JSON_DICT_PARAMETERIZED_TYPE) - val payload = adapter.fromJson(payloadString) - - if (payload == null) { - Timber.tag(loggerTag.value).e("## decryptEvent failed : null payload") - throw MXCryptoError.Base(MXCryptoError.ErrorType.UNABLE_TO_DECRYPT, MXCryptoError.MISSING_CIPHER_TEXT_REASON) - } - - val olmPayloadContent = OlmPayloadContent.fromJsonString(payloadString) ?: run { - Timber.tag(loggerTag.value).e("## decryptEvent() : bad olmPayloadContent format") - throw MXCryptoError.Base(MXCryptoError.ErrorType.BAD_DECRYPTED_FORMAT, MXCryptoError.BAD_DECRYPTED_FORMAT_TEXT_REASON) - } - - if (olmPayloadContent.recipient.isNullOrBlank()) { - val reason = String.format(MXCryptoError.ERROR_MISSING_PROPERTY_REASON, "recipient") - Timber.tag(loggerTag.value).e("## decryptEvent() : $reason") - throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_PROPERTY, reason) - } - - if (olmPayloadContent.recipient != userId) { - Timber.tag(loggerTag.value).e( - "## decryptEvent() : Event ${event.eventId}:" + - " Intended recipient ${olmPayloadContent.recipient} does not match our id $userId" - ) - throw MXCryptoError.Base( - MXCryptoError.ErrorType.BAD_RECIPIENT, - String.format(MXCryptoError.BAD_RECIPIENT_REASON, olmPayloadContent.recipient) - ) - } - - val recipientKeys = olmPayloadContent.recipientKeys ?: run { - Timber.tag(loggerTag.value).e( - "## decryptEvent() : Olm event (id=${event.eventId}) contains no 'recipient_keys'" + - " property; cannot prevent unknown-key attack" - ) - throw MXCryptoError.Base( - MXCryptoError.ErrorType.MISSING_PROPERTY, - String.format(MXCryptoError.ERROR_MISSING_PROPERTY_REASON, "recipient_keys") - ) - } - - val ed25519 = recipientKeys["ed25519"] - - if (ed25519 != olmDevice.deviceEd25519Key) { - Timber.tag(loggerTag.value).e("## decryptEvent() : Event ${event.eventId}: Intended recipient ed25519 key $ed25519 did not match ours") - throw MXCryptoError.Base( - MXCryptoError.ErrorType.BAD_RECIPIENT_KEY, - MXCryptoError.BAD_RECIPIENT_KEY_REASON - ) - } - - if (olmPayloadContent.sender.isNullOrBlank()) { - Timber.tag(loggerTag.value) - .e("## decryptEvent() : Olm event (id=${event.eventId}) contains no 'sender' property; cannot prevent unknown-key attack") - throw MXCryptoError.Base( - MXCryptoError.ErrorType.MISSING_PROPERTY, - String.format(MXCryptoError.ERROR_MISSING_PROPERTY_REASON, "sender") - ) - } - - if (olmPayloadContent.sender != event.senderId) { - Timber.tag(loggerTag.value) - .e("Event ${event.eventId}: sender ${olmPayloadContent.sender} does not match reported sender ${event.senderId}") - throw MXCryptoError.Base( - MXCryptoError.ErrorType.FORWARDED_MESSAGE, - String.format(MXCryptoError.FORWARDED_MESSAGE_REASON, olmPayloadContent.sender) - ) - } - - if (olmPayloadContent.roomId != event.roomId) { - Timber.tag(loggerTag.value) - .e("## decryptEvent() : Event ${event.eventId}: room ${olmPayloadContent.roomId} does not match reported room ${event.roomId}") - throw MXCryptoError.Base( - MXCryptoError.ErrorType.BAD_ROOM, - String.format(MXCryptoError.BAD_ROOM_REASON, olmPayloadContent.roomId) - ) - } - - val keys = olmPayloadContent.keys ?: run { - Timber.tag(loggerTag.value).e("## decryptEvent failed : null keys") - throw MXCryptoError.Base( - MXCryptoError.ErrorType.UNABLE_TO_DECRYPT, - MXCryptoError.MISSING_CIPHER_TEXT_REASON - ) - } - - return MXEventDecryptionResult( - clearEvent = payload, - senderCurve25519Key = senderKey, - claimedEd25519Key = keys["ed25519"] - ) - } - - /** - * Attempt to decrypt an Olm message. - * - * @param message message object, with 'type' and 'body' fields. - * @param theirDeviceIdentityKey the Curve25519 identity key of the sender. - * @return payload, if decrypted successfully. - */ - private suspend fun decryptMessage(message: JsonDict, theirDeviceIdentityKey: String): String? { - val sessionIds = olmDevice.getSessionIds(theirDeviceIdentityKey) - - val messageBody = message["body"] as? String ?: return null - val messageType = when (val typeAsVoid = message["type"]) { - is Double -> typeAsVoid.toInt() - is Int -> typeAsVoid - is Long -> typeAsVoid.toInt() - else -> return null - } - - // Try each session in turn - // decryptionErrors = {}; - - val isPreKey = messageType == 0 - // we want to synchronize on prekey if not we could end up create two olm sessions - // Not very clear but it looks like the js-sdk for consistency - return if (isPreKey) { - olmDevice.mutex.withLock { - reallyDecryptMessage(sessionIds, messageBody, messageType, theirDeviceIdentityKey) - } - } else { - reallyDecryptMessage(sessionIds, messageBody, messageType, theirDeviceIdentityKey) - } - } - - private suspend fun reallyDecryptMessage(sessionIds: List, messageBody: String, messageType: Int, theirDeviceIdentityKey: String): String? { - Timber.tag(loggerTag.value).d("decryptMessage() try to decrypt olm message type:$messageType from ${sessionIds.size} known sessions") - for (sessionId in sessionIds) { - val payload = try { - olmDevice.decryptMessage(messageBody, messageType, sessionId, theirDeviceIdentityKey) - } catch (throwable: Exception) { - // As we are trying one by one, we don't really care of the error here - Timber.tag(loggerTag.value).d("decryptMessage() failed with session $sessionId") - null - } - - if (null != payload) { - Timber.tag(loggerTag.value).v("## decryptMessage() : Decrypted Olm message from $theirDeviceIdentityKey with session $sessionId") - return payload - } else { - val foundSession = olmDevice.matchesSession(theirDeviceIdentityKey, sessionId, messageType, messageBody) - - if (foundSession) { - // Decryption failed, but it was a prekey message matching this - // session, so it should have worked. - Timber.tag(loggerTag.value).e("## decryptMessage() : Error decrypting prekey message with existing session id $sessionId:TODO") - return null - } - } - } - - if (messageType != 0) { - // not a prekey message, so it should have matched an existing session, but it - // didn't work. - - if (sessionIds.isEmpty()) { - Timber.tag(loggerTag.value).e("## decryptMessage() : No existing sessions") - } else { - Timber.tag(loggerTag.value).e("## decryptMessage() : Error decrypting non-prekey message with existing sessions") - } - - return null - } - - // prekey message which doesn't match any existing sessions: make a new - // session. - // XXXX Possible races here? if concurrent access for same prekey message, we might create 2 sessions? - Timber.tag(loggerTag.value).d("## decryptMessage() : Create inbound group session from prekey sender:$theirDeviceIdentityKey") - - val res = olmDevice.createInboundSession(theirDeviceIdentityKey, messageType, messageBody) - - if (null == res) { - Timber.tag(loggerTag.value).e("## decryptMessage() : Error decrypting non-prekey message with existing sessions") - return null - } - - Timber.tag(loggerTag.value).v("## decryptMessage() : Created new inbound Olm session get id ${res["session_id"]} with $theirDeviceIdentityKey") - - return res["payload"] - } -} diff --git a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/algorithms/olm/MXOlmDecryptionFactory.kt b/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/algorithms/olm/MXOlmDecryptionFactory.kt deleted file mode 100644 index a50ac8ca8a..0000000000 --- a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/algorithms/olm/MXOlmDecryptionFactory.kt +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright 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. - */ - -package org.matrix.android.sdk.internal.crypto.algorithms.olm - -import org.matrix.android.sdk.internal.crypto.MXOlmDevice -import org.matrix.android.sdk.internal.di.UserId -import javax.inject.Inject - -internal class MXOlmDecryptionFactory @Inject constructor( - private val olmDevice: MXOlmDevice, - @UserId private val userId: String -) { - - fun create(): MXOlmDecryption { - return MXOlmDecryption( - olmDevice, - userId - ) - } -} diff --git a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/algorithms/olm/MXOlmEncryption.kt b/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/algorithms/olm/MXOlmEncryption.kt deleted file mode 100644 index 6f4f316800..0000000000 --- a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/algorithms/olm/MXOlmEncryption.kt +++ /dev/null @@ -1,80 +0,0 @@ -/* - * 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. - */ - -package org.matrix.android.sdk.internal.crypto.algorithms.olm - -import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo -import org.matrix.android.sdk.api.session.events.model.Content -import org.matrix.android.sdk.api.session.events.model.toContent -import org.matrix.android.sdk.internal.crypto.DeviceListManager -import org.matrix.android.sdk.internal.crypto.MXOlmDevice -import org.matrix.android.sdk.internal.crypto.actions.EnsureOlmSessionsForUsersAction -import org.matrix.android.sdk.internal.crypto.actions.MessageEncrypter -import org.matrix.android.sdk.internal.crypto.algorithms.IMXEncrypting -import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore - -internal class MXOlmEncryption( - private val roomId: String, - private val olmDevice: MXOlmDevice, - private val cryptoStore: IMXCryptoStore, - private val messageEncrypter: MessageEncrypter, - private val deviceListManager: DeviceListManager, - private val ensureOlmSessionsForUsersAction: EnsureOlmSessionsForUsersAction -) : - IMXEncrypting { - - override suspend fun encryptEventContent(eventContent: Content, eventType: String, userIds: List): Content { - // pick the list of recipients based on the membership list. - // - // TODO there is a race condition here! What if a new user turns up - ensureSession(userIds) - val deviceInfos = ArrayList() - for (userId in userIds) { - val devices = cryptoStore.getUserDevices(userId)?.values.orEmpty() - for (device in devices) { - val key = device.identityKey() - if (key == olmDevice.deviceCurve25519Key) { - // Don't bother setting up session to ourself - continue - } - if (device.isBlocked) { - // Don't bother setting up sessions with blocked users - continue - } - deviceInfos.add(device) - } - } - - val messageMap = mapOf( - "room_id" to roomId, - "type" to eventType, - "content" to eventContent - ) - - messageEncrypter.encryptMessage(messageMap, deviceInfos) - return messageMap.toContent() - } - - /** - * Ensure that the session. - * - * @param users the user ids list - */ - private suspend fun ensureSession(users: List) { - deviceListManager.downloadKeys(users, false) - ensureOlmSessionsForUsersAction.handle(users) - } -} diff --git a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/algorithms/olm/MXOlmEncryptionFactory.kt b/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/algorithms/olm/MXOlmEncryptionFactory.kt deleted file mode 100644 index 012886203e..0000000000 --- a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/algorithms/olm/MXOlmEncryptionFactory.kt +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright 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. - */ - -package org.matrix.android.sdk.internal.crypto.algorithms.olm - -import org.matrix.android.sdk.api.MatrixCoroutineDispatchers -import org.matrix.android.sdk.internal.crypto.DeviceListManager -import org.matrix.android.sdk.internal.crypto.MXOlmDevice -import org.matrix.android.sdk.internal.crypto.actions.EnsureOlmSessionsForUsersAction -import org.matrix.android.sdk.internal.crypto.actions.MessageEncrypter -import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore -import javax.inject.Inject - -internal class MXOlmEncryptionFactory @Inject constructor( - private val olmDevice: MXOlmDevice, - private val cryptoStore: IMXCryptoStore, - private val messageEncrypter: MessageEncrypter, - private val deviceListManager: DeviceListManager, - private val coroutineDispatchers: MatrixCoroutineDispatchers, - private val ensureOlmSessionsForUsersAction: EnsureOlmSessionsForUsersAction -) { - - fun create(roomId: String): MXOlmEncryption { - return MXOlmEncryption( - roomId, - olmDevice, - cryptoStore, - messageEncrypter, - deviceListManager, - ensureOlmSessionsForUsersAction - ) - } -} diff --git a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/crosssigning/ComputeTrustTask.kt b/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/crosssigning/ComputeTrustTask.kt deleted file mode 100644 index 02ea943284..0000000000 --- a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/crosssigning/ComputeTrustTask.kt +++ /dev/null @@ -1,93 +0,0 @@ -/* - * Copyright 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. - */ -package org.matrix.android.sdk.internal.crypto.crosssigning - -import kotlinx.coroutines.withContext -import org.matrix.android.sdk.api.MatrixCoroutineDispatchers -import org.matrix.android.sdk.api.extensions.orFalse -import org.matrix.android.sdk.api.session.crypto.crosssigning.MXCrossSigningInfo -import org.matrix.android.sdk.api.session.crypto.model.RoomEncryptionTrustLevel -import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore -import org.matrix.android.sdk.internal.di.UserId -import org.matrix.android.sdk.internal.task.Task -import javax.inject.Inject - -internal interface ComputeTrustTask : Task { - data class Params( - val activeMemberUserIds: List, - val isDirectRoom: Boolean - ) -} - -internal class DefaultComputeTrustTask @Inject constructor( - private val cryptoStore: IMXCryptoStore, - @UserId private val userId: String, - private val coroutineDispatchers: MatrixCoroutineDispatchers -) : ComputeTrustTask { - - override suspend fun execute(params: ComputeTrustTask.Params): RoomEncryptionTrustLevel = withContext(coroutineDispatchers.crypto) { - // The set of “all users” depends on the type of room: - // For regular / topic rooms, all users including yourself, are considered when decorating a room - // For 1:1 and group DM rooms, all other users (i.e. excluding yourself) are considered when decorating a room - val listToCheck = if (params.isDirectRoom) { - params.activeMemberUserIds.filter { it != userId } - } else { - params.activeMemberUserIds - } - - val allTrustedUserIds = listToCheck - .filter { userId -> getUserCrossSigningKeys(userId)?.isTrusted() == true } - - if (allTrustedUserIds.isEmpty()) { - RoomEncryptionTrustLevel.Default - } else { - // If one of the verified user as an untrusted device -> warning - // If all devices of all verified users are trusted -> green - // else -> black - allTrustedUserIds - .mapNotNull { cryptoStore.getUserDeviceList(it) } - .flatten() - .let { allDevices -> - if (getMyCrossSigningKeys() != null) { - allDevices.any { !it.trustLevel?.crossSigningVerified.orFalse() } - } else { - // Legacy method - allDevices.any { !it.isVerified } - } - } - .let { hasWarning -> - if (hasWarning) { - RoomEncryptionTrustLevel.Warning - } else { - if (listToCheck.size == allTrustedUserIds.size) { - // all users are trusted and all devices are verified - RoomEncryptionTrustLevel.Trusted - } else { - RoomEncryptionTrustLevel.Default - } - } - } - } - } - - private fun getUserCrossSigningKeys(otherUserId: String): MXCrossSigningInfo? { - return cryptoStore.getCrossSigningInfo(otherUserId) - } - - private fun getMyCrossSigningKeys(): MXCrossSigningInfo? { - return cryptoStore.getMyCrossSigningInfo() - } -} diff --git a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/crosssigning/CrossSigningOlm.kt b/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/crosssigning/CrossSigningOlm.kt deleted file mode 100644 index 0f29404d4f..0000000000 --- a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/crosssigning/CrossSigningOlm.kt +++ /dev/null @@ -1,91 +0,0 @@ -/* - * Copyright 2022 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. - */ - -package org.matrix.android.sdk.internal.crypto.crosssigning - -import org.matrix.android.sdk.api.util.JsonDict -import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore -import org.matrix.android.sdk.internal.session.SessionScope -import org.matrix.android.sdk.internal.util.JsonCanonicalizer -import org.matrix.olm.OlmPkSigning -import org.matrix.olm.OlmUtility -import javax.inject.Inject - -/** - * Holds the OlmPkSigning for cross signing. - * Can be injected without having to get the full cross signing service - */ -@SessionScope -internal class CrossSigningOlm @Inject constructor( - private val cryptoStore: IMXCryptoStore, -) { - - enum class KeyType { - SELF, - USER, - MASTER - } - - var olmUtility: OlmUtility = OlmUtility() - - var masterPkSigning: OlmPkSigning? = null - var userPkSigning: OlmPkSigning? = null - var selfSigningPkSigning: OlmPkSigning? = null - - fun release() { - olmUtility.releaseUtility() - listOf(masterPkSigning, userPkSigning, selfSigningPkSigning).forEach { it?.releaseSigning() } - } - - fun signObject(type: KeyType, strToSign: String): Map { - val myKeys = cryptoStore.getMyCrossSigningInfo() - val pubKey = when (type) { - KeyType.SELF -> myKeys?.selfSigningKey() - KeyType.USER -> myKeys?.userKey() - KeyType.MASTER -> myKeys?.masterKey() - }?.unpaddedBase64PublicKey - val pkSigning = when (type) { - KeyType.SELF -> selfSigningPkSigning - KeyType.USER -> userPkSigning - KeyType.MASTER -> masterPkSigning - } - if (pubKey == null || pkSigning == null) { - throw Throwable("Cannot sign from this account, public and/or privateKey Unknown $type|$pkSigning") - } - val signature = pkSigning.sign(strToSign) - return mapOf( - "ed25519:$pubKey" to signature - ) - } - - fun verifySignature(type: KeyType, signable: JsonDict, signatures: Map>) { - val myKeys = cryptoStore.getMyCrossSigningInfo() - ?: throw NoSuchElementException("Cross Signing not configured") - val myUserID = myKeys.userId - val pubKey = when (type) { - KeyType.SELF -> myKeys.selfSigningKey() - KeyType.USER -> myKeys.userKey() - KeyType.MASTER -> myKeys.masterKey() - }?.unpaddedBase64PublicKey ?: throw NoSuchElementException("Cross Signing not configured") - val signaturesMadeByMyKey = signatures[myUserID] // Signatures made by me - ?.get("ed25519:$pubKey") - - require(signaturesMadeByMyKey.orEmpty().isNotBlank()) { "Not signed with my key $type" } - - // Check that Alice USK signature of Bob MSK is valid - olmUtility.verifyEd25519Signature(signaturesMadeByMyKey, pubKey, JsonCanonicalizer.getCanonicalJson(Map::class.java, signable)) - } -} diff --git a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/crosssigning/DefaultCrossSigningService.kt b/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/crosssigning/DefaultCrossSigningService.kt deleted file mode 100644 index e020946484..0000000000 --- a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/crosssigning/DefaultCrossSigningService.kt +++ /dev/null @@ -1,819 +0,0 @@ -/* - * Copyright 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. - */ - -package org.matrix.android.sdk.internal.crypto.crosssigning - -import androidx.lifecycle.LiveData -import androidx.work.BackoffPolicy -import androidx.work.ExistingWorkPolicy -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.launch -import kotlinx.coroutines.runBlocking -import kotlinx.coroutines.withContext -import org.matrix.android.sdk.api.MatrixCoroutineDispatchers -import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor -import org.matrix.android.sdk.api.extensions.orFalse -import org.matrix.android.sdk.api.session.crypto.crosssigning.CrossSigningService -import org.matrix.android.sdk.api.session.crypto.crosssigning.DeviceTrustLevel -import org.matrix.android.sdk.api.session.crypto.crosssigning.DeviceTrustResult -import org.matrix.android.sdk.api.session.crypto.crosssigning.MXCrossSigningInfo -import org.matrix.android.sdk.api.session.crypto.crosssigning.PrivateKeysInfo -import org.matrix.android.sdk.api.session.crypto.crosssigning.UserTrustResult -import org.matrix.android.sdk.api.session.crypto.crosssigning.isCrossSignedVerified -import org.matrix.android.sdk.api.session.crypto.crosssigning.isLocallyVerified -import org.matrix.android.sdk.api.session.crypto.crosssigning.isVerified -import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo -import org.matrix.android.sdk.api.session.crypto.model.RoomEncryptionTrustLevel -import org.matrix.android.sdk.api.util.Optional -import org.matrix.android.sdk.api.util.fromBase64 -import org.matrix.android.sdk.internal.crypto.DeviceListManager -import org.matrix.android.sdk.internal.crypto.OutgoingKeyRequestManager -import org.matrix.android.sdk.internal.crypto.model.rest.UploadSignatureQueryBuilder -import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore -import org.matrix.android.sdk.internal.crypto.tasks.InitializeCrossSigningTask -import org.matrix.android.sdk.internal.crypto.tasks.UploadSignaturesTask -import org.matrix.android.sdk.internal.di.SessionId -import org.matrix.android.sdk.internal.di.UserId -import org.matrix.android.sdk.internal.di.WorkManagerProvider -import org.matrix.android.sdk.internal.session.SessionScope -import org.matrix.android.sdk.internal.util.JsonCanonicalizer -import org.matrix.android.sdk.internal.util.logLimit -import org.matrix.android.sdk.internal.worker.WorkerParamsFactory -import org.matrix.olm.OlmPkSigning -import timber.log.Timber -import java.util.concurrent.TimeUnit -import javax.inject.Inject - -@SessionScope -internal class DefaultCrossSigningService @Inject constructor( - @UserId private val myUserId: String, - @SessionId private val sessionId: String, - private val cryptoStore: IMXCryptoStore, - private val deviceListManager: DeviceListManager, - private val initializeCrossSigningTask: InitializeCrossSigningTask, - private val uploadSignaturesTask: UploadSignaturesTask, - private val coroutineDispatchers: MatrixCoroutineDispatchers, - private val cryptoCoroutineScope: CoroutineScope, - private val workManagerProvider: WorkManagerProvider, - private val outgoingKeyRequestManager: OutgoingKeyRequestManager, - private val crossSigningOlm: CrossSigningOlm, - private val updateTrustWorkerDataRepository: UpdateTrustWorkerDataRepository -) : CrossSigningService, - DeviceListManager.UserDevicesUpdateListener { - - init { - try { - - // Try to get stored keys if they exist - cryptoStore.getMyCrossSigningInfo()?.let { mxCrossSigningInfo -> - Timber.i("## CrossSigning - Found Existing self signed keys") - Timber.i("## CrossSigning - Checking if private keys are known") - - cryptoStore.getCrossSigningPrivateKeys()?.let { privateKeysInfo -> - privateKeysInfo.master - ?.fromBase64() - ?.let { privateKeySeed -> - val pkSigning = OlmPkSigning() - if (pkSigning.initWithSeed(privateKeySeed) == mxCrossSigningInfo.masterKey()?.unpaddedBase64PublicKey) { - crossSigningOlm.masterPkSigning = pkSigning - Timber.i("## CrossSigning - Loading master key success") - } else { - Timber.w("## CrossSigning - Public master key does not match the private key") - pkSigning.releaseSigning() - // TODO untrust? - } - } - privateKeysInfo.user - ?.fromBase64() - ?.let { privateKeySeed -> - val pkSigning = OlmPkSigning() - if (pkSigning.initWithSeed(privateKeySeed) == mxCrossSigningInfo.userKey()?.unpaddedBase64PublicKey) { - crossSigningOlm.userPkSigning = pkSigning - Timber.i("## CrossSigning - Loading User Signing key success") - } else { - Timber.w("## CrossSigning - Public User key does not match the private key") - pkSigning.releaseSigning() - // TODO untrust? - } - } - privateKeysInfo.selfSigned - ?.fromBase64() - ?.let { privateKeySeed -> - val pkSigning = OlmPkSigning() - if (pkSigning.initWithSeed(privateKeySeed) == mxCrossSigningInfo.selfSigningKey()?.unpaddedBase64PublicKey) { - crossSigningOlm.selfSigningPkSigning = pkSigning - Timber.i("## CrossSigning - Loading Self Signing key success") - } else { - Timber.w("## CrossSigning - Public Self Signing key does not match the private key") - pkSigning.releaseSigning() - // TODO untrust? - } - } - } - - // Recover local trust in case private key are there? - cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { - setUserKeysAsTrusted(myUserId, checkUserTrust(myUserId).isVerified()) - } - } - } catch (e: Throwable) { - // Mmm this kind of a big issue - Timber.e(e, "Failed to initialize Cross Signing") - } - - deviceListManager.addListener(this) - } - - fun release() { - crossSigningOlm.release() - deviceListManager.removeListener(this) - } - - protected fun finalize() { - release() - } - - /** - * - Make 3 key pairs (MSK, USK, SSK) - * - Save the private keys with proper security - * - Sign the keys and upload them - * - Sign the current device with SSK and sign MSK with device key (migration) and upload signatures. - */ - override suspend fun initializeCrossSigning(uiaInterceptor: UserInteractiveAuthInterceptor?) { - Timber.d("## CrossSigning initializeCrossSigning") - - val params = InitializeCrossSigningTask.Params( - interactiveAuthInterceptor = uiaInterceptor - ) - val data = initializeCrossSigningTask - .execute(params) - val crossSigningInfo = MXCrossSigningInfo( - myUserId, - listOf(data.masterKeyInfo, data.userKeyInfo, data.selfSignedKeyInfo), - true - ) - withContext(coroutineDispatchers.crypto) { - cryptoStore.setMyCrossSigningInfo(crossSigningInfo) - setUserKeysAsTrusted(myUserId, true) - cryptoStore.storePrivateKeysInfo(data.masterKeyPK, data.userKeyPK, data.selfSigningKeyPK) - crossSigningOlm.masterPkSigning = OlmPkSigning().apply { initWithSeed(data.masterKeyPK.fromBase64()) } - crossSigningOlm.userPkSigning = OlmPkSigning().apply { initWithSeed(data.userKeyPK.fromBase64()) } - crossSigningOlm.selfSigningPkSigning = OlmPkSigning().apply { initWithSeed(data.selfSigningKeyPK.fromBase64()) } - } - } - - override suspend fun onSecretMSKGossip(mskPrivateKey: String) { - Timber.i("## CrossSigning - onSecretSSKGossip") - val mxCrossSigningInfo = getMyCrossSigningKeys() ?: return Unit.also { - Timber.e("## CrossSigning - onSecretMSKGossip() received secret but public key is not known") - } - - mskPrivateKey.fromBase64() - .let { privateKeySeed -> - val pkSigning = OlmPkSigning() - try { - if (pkSigning.initWithSeed(privateKeySeed) == mxCrossSigningInfo.masterKey()?.unpaddedBase64PublicKey) { - crossSigningOlm.masterPkSigning?.releaseSigning() - crossSigningOlm.masterPkSigning = pkSigning - Timber.i("## CrossSigning - Loading MSK success") - cryptoStore.storeMSKPrivateKey(mskPrivateKey) - return - } else { - Timber.e("## CrossSigning - onSecretMSKGossip() private key do not match public key") - pkSigning.releaseSigning() - } - } catch (failure: Throwable) { - Timber.e("## CrossSigning - onSecretMSKGossip() ${failure.localizedMessage}") - pkSigning.releaseSigning() - } - } - } - - override suspend fun onSecretSSKGossip(sskPrivateKey: String) { - Timber.i("## CrossSigning - onSecretSSKGossip") - val mxCrossSigningInfo = getMyCrossSigningKeys() ?: return Unit.also { - Timber.e("## CrossSigning - onSecretSSKGossip() received secret but public key is not known") - } - - sskPrivateKey.fromBase64() - .let { privateKeySeed -> - val pkSigning = OlmPkSigning() - try { - if (pkSigning.initWithSeed(privateKeySeed) == mxCrossSigningInfo.selfSigningKey()?.unpaddedBase64PublicKey) { - crossSigningOlm.selfSigningPkSigning?.releaseSigning() - crossSigningOlm.selfSigningPkSigning = pkSigning - Timber.i("## CrossSigning - Loading SSK success") - cryptoStore.storeSSKPrivateKey(sskPrivateKey) - return - } else { - Timber.e("## CrossSigning - onSecretSSKGossip() private key do not match public key") - pkSigning.releaseSigning() - } - } catch (failure: Throwable) { - Timber.e("## CrossSigning - onSecretSSKGossip() ${failure.localizedMessage}") - pkSigning.releaseSigning() - } - } - } - - override suspend fun onSecretUSKGossip(uskPrivateKey: String) { - Timber.i("## CrossSigning - onSecretUSKGossip") - val mxCrossSigningInfo = getMyCrossSigningKeys() ?: return Unit.also { - Timber.e("## CrossSigning - onSecretUSKGossip() received secret but public key is not knwow ") - } - - uskPrivateKey.fromBase64() - .let { privateKeySeed -> - val pkSigning = OlmPkSigning() - try { - if (pkSigning.initWithSeed(privateKeySeed) == mxCrossSigningInfo.userKey()?.unpaddedBase64PublicKey) { - crossSigningOlm.userPkSigning?.releaseSigning() - crossSigningOlm.userPkSigning = pkSigning - Timber.i("## CrossSigning - Loading USK success") - cryptoStore.storeUSKPrivateKey(uskPrivateKey) - return - } else { - Timber.e("## CrossSigning - onSecretUSKGossip() private key do not match public key") - pkSigning.releaseSigning() - } - } catch (failure: Throwable) { - pkSigning.releaseSigning() - } - } - } - - override suspend fun checkTrustFromPrivateKeys( - masterKeyPrivateKey: String?, - uskKeyPrivateKey: String?, - sskPrivateKey: String? - ): UserTrustResult { - val mxCrossSigningInfo = getMyCrossSigningKeys() ?: return UserTrustResult.CrossSigningNotConfigured(myUserId) - - var masterKeyIsTrusted = false - var userKeyIsTrusted = false - var selfSignedKeyIsTrusted = false - - masterKeyPrivateKey?.fromBase64() - ?.let { privateKeySeed -> - val pkSigning = OlmPkSigning() - try { - if (pkSigning.initWithSeed(privateKeySeed) == mxCrossSigningInfo.masterKey()?.unpaddedBase64PublicKey) { - crossSigningOlm.masterPkSigning?.releaseSigning() - crossSigningOlm.masterPkSigning = pkSigning - masterKeyIsTrusted = true - Timber.i("## CrossSigning - Loading master key success") - } else { - pkSigning.releaseSigning() - } - } catch (failure: Throwable) { - pkSigning.releaseSigning() - } - } - - uskKeyPrivateKey?.fromBase64() - ?.let { privateKeySeed -> - val pkSigning = OlmPkSigning() - try { - if (pkSigning.initWithSeed(privateKeySeed) == mxCrossSigningInfo.userKey()?.unpaddedBase64PublicKey) { - crossSigningOlm.userPkSigning?.releaseSigning() - crossSigningOlm.userPkSigning = pkSigning - userKeyIsTrusted = true - Timber.i("## CrossSigning - Loading master key success") - } else { - pkSigning.releaseSigning() - } - } catch (failure: Throwable) { - pkSigning.releaseSigning() - } - } - - sskPrivateKey?.fromBase64() - ?.let { privateKeySeed -> - val pkSigning = OlmPkSigning() - try { - if (pkSigning.initWithSeed(privateKeySeed) == mxCrossSigningInfo.selfSigningKey()?.unpaddedBase64PublicKey) { - crossSigningOlm.selfSigningPkSigning?.releaseSigning() - crossSigningOlm.selfSigningPkSigning = pkSigning - selfSignedKeyIsTrusted = true - Timber.i("## CrossSigning - Loading master key success") - } else { - pkSigning.releaseSigning() - } - } catch (failure: Throwable) { - pkSigning.releaseSigning() - } - } - - if (!masterKeyIsTrusted || !userKeyIsTrusted || !selfSignedKeyIsTrusted) { - return UserTrustResult.Failure("Keys not trusted $mxCrossSigningInfo") // UserTrustResult.KeysNotTrusted(mxCrossSigningInfo) - } else { - cryptoStore.markMyMasterKeyAsLocallyTrusted(true) - val checkSelfTrust = checkSelfTrust() - if (checkSelfTrust.isVerified()) { - cryptoStore.storePrivateKeysInfo(masterKeyPrivateKey, uskKeyPrivateKey, sskPrivateKey) - setUserKeysAsTrusted(myUserId, true) - } - return checkSelfTrust - } - } - - /** - * - * ┏━━━━━━━━┓ ┏━━━━━━━━┓ - * ┃ ALICE ┃ ┃ BOB ┃ - * ┗━━━━━━━━┛ ┗━━━━━━━━┛ - * MSK ┌────────────▶ MSK - * │ - * │ │ - * │ SSK │ - * │ │ - * │ │ - * └──▶ USK ────────────┘ - * . - */ - override suspend fun isUserTrusted(otherUserId: String): Boolean { - return withContext(coroutineDispatchers.io) { - cryptoStore.getCrossSigningInfo(otherUserId)?.isTrusted() == true - } - } - - override suspend fun isCrossSigningVerified(): Boolean { - return withContext(coroutineDispatchers.io) { - checkSelfTrust().isVerified() - } - } - - /** - * Will not force a download of the key, but will verify signatures trust chain. - */ - override suspend fun checkUserTrust(otherUserId: String): UserTrustResult { - Timber.v("## CrossSigning checkUserTrust for $otherUserId") - if (otherUserId == myUserId) { - return checkSelfTrust() - } - // I trust a user if I trust his master key - // I can trust the master key if it is signed by my user key - // TODO what if the master key is signed by a device key that i have verified - - // First let's get my user key - val myCrossSigningInfo = cryptoStore.getCrossSigningInfo(myUserId) - - return checkOtherMSKTrusted(myCrossSigningInfo, cryptoStore.getCrossSigningInfo(otherUserId)) - } - - override fun checkOtherMSKTrusted(myCrossSigningInfo: MXCrossSigningInfo?, otherInfo: MXCrossSigningInfo?): UserTrustResult { - val myUserKey = myCrossSigningInfo?.userKey() - ?: return UserTrustResult.CrossSigningNotConfigured(myUserId) - - if (!myCrossSigningInfo.isTrusted()) { - return UserTrustResult.Failure("Keys not trusted $myCrossSigningInfo") // UserTrustResult.KeysNotTrusted(myCrossSigningInfo) - } - - // Let's get the other user master key - val otherMasterKey = otherInfo?.masterKey() - ?: return UserTrustResult.Failure("Unknown MSK for ${otherInfo?.userId}") // UserTrustResult.UnknownCrossSignatureInfo(otherInfo?.userId ?: "") - - val masterKeySignaturesMadeByMyUserKey = otherMasterKey.signatures - ?.get(myUserId) // Signatures made by me - ?.get("ed25519:${myUserKey.unpaddedBase64PublicKey}") - - if (masterKeySignaturesMadeByMyUserKey.isNullOrBlank()) { - Timber.d("## CrossSigning checkUserTrust false for ${otherInfo.userId}, not signed by my UserSigningKey") - return UserTrustResult.Failure("MSK not signed by my USK $otherMasterKey") // UserTrustResult.KeyNotSigned(otherMasterKey) - } - - // Check that Alice USK signature of Bob MSK is valid - try { - crossSigningOlm.olmUtility.verifyEd25519Signature( - masterKeySignaturesMadeByMyUserKey, - myUserKey.unpaddedBase64PublicKey, - otherMasterKey.canonicalSignable() - ) - } catch (failure: Throwable) { - return UserTrustResult.Failure("Invalid signature $masterKeySignaturesMadeByMyUserKey") // UserTrustResult.InvalidSignature(myUserKey, masterKeySignaturesMadeByMyUserKey) - } - - return UserTrustResult.Success - } - - private fun checkSelfTrust(): UserTrustResult { - // Special case when it's me, - // I have to check that MSK -> USK -> SSK - // and that MSK is trusted (i know the private key, or is signed by a trusted device) - val myCrossSigningInfo = cryptoStore.getCrossSigningInfo(myUserId) - - return checkSelfTrust(myCrossSigningInfo, cryptoStore.getUserDeviceList(myUserId)) - } - - override fun checkSelfTrust(myCrossSigningInfo: MXCrossSigningInfo?, myDevices: List?): UserTrustResult { - // Special case when it's me, - // I have to check that MSK -> USK -> SSK - // and that MSK is trusted (i know the private key, or is signed by a trusted device) -// val myCrossSigningInfo = cryptoStore.getCrossSigningInfo(userId) - - val myMasterKey = myCrossSigningInfo?.masterKey() - ?: return UserTrustResult.CrossSigningNotConfigured(myUserId) - - // Is the master key trusted - // 1) check if I know the private key - val masterPrivateKey = cryptoStore.getCrossSigningPrivateKeys() - ?.master - ?.fromBase64() - - var isMaterKeyTrusted = false - if (myMasterKey.trustLevel?.locallyVerified == true) { - isMaterKeyTrusted = true - } else if (masterPrivateKey != null) { - // Check if private match public - var olmPkSigning: OlmPkSigning? = null - try { - olmPkSigning = OlmPkSigning() - val expectedPK = olmPkSigning.initWithSeed(masterPrivateKey) - isMaterKeyTrusted = myMasterKey.unpaddedBase64PublicKey == expectedPK - } catch (failure: Throwable) { - Timber.e(failure) - } - olmPkSigning?.releaseSigning() - } else { - // Maybe it's signed by a locally trusted device? - myMasterKey.signatures?.get(myUserId)?.forEach { (key, value) -> - val potentialDeviceId = key.removePrefix("ed25519:") - val potentialDevice = myDevices?.firstOrNull { it.deviceId == potentialDeviceId } // cryptoStore.getUserDevice(userId, potentialDeviceId) - if (potentialDevice != null && potentialDevice.isVerified) { - // Check signature validity? - try { - crossSigningOlm.olmUtility.verifyEd25519Signature(value, potentialDevice.fingerprint(), myMasterKey.canonicalSignable()) - isMaterKeyTrusted = true - return@forEach - } catch (failure: Throwable) { - // log - Timber.w(failure, "Signature not valid?") - } - } - } - } - - if (!isMaterKeyTrusted) { - return UserTrustResult.Failure("Keys not trusted $myCrossSigningInfo") // UserTrustResult.KeysNotTrusted(myCrossSigningInfo) - } - - val myUserKey = myCrossSigningInfo.userKey() - ?: return UserTrustResult.CrossSigningNotConfigured(myUserId) - - val userKeySignaturesMadeByMyMasterKey = myUserKey.signatures - ?.get(myUserId) // Signatures made by me - ?.get("ed25519:${myMasterKey.unpaddedBase64PublicKey}") - - if (userKeySignaturesMadeByMyMasterKey.isNullOrBlank()) { - Timber.d("## CrossSigning checkUserTrust false for $myUserId, USK not signed by MSK") - return UserTrustResult.Failure("USK not signed by MSK") // UserTrustResult.KeyNotSigned(myUserKey) - } - - // Check that Alice USK signature of Alice MSK is valid - try { - crossSigningOlm.olmUtility.verifyEd25519Signature( - userKeySignaturesMadeByMyMasterKey, - myMasterKey.unpaddedBase64PublicKey, - myUserKey.canonicalSignable() - ) - } catch (failure: Throwable) { - return UserTrustResult.Failure("Invalid MSK signature of USK") // UserTrustResult.InvalidSignature(myUserKey, userKeySignaturesMadeByMyMasterKey) - } - - val mySSKey = myCrossSigningInfo.selfSigningKey() - ?: return UserTrustResult.CrossSigningNotConfigured(myUserId) - - val ssKeySignaturesMadeByMyMasterKey = mySSKey.signatures - ?.get(myUserId) // Signatures made by me - ?.get("ed25519:${myMasterKey.unpaddedBase64PublicKey}") - - if (ssKeySignaturesMadeByMyMasterKey.isNullOrBlank()) { - Timber.d("## CrossSigning checkUserTrust false for $myUserId, SSK not signed by MSK") - return UserTrustResult.Failure("SSK not signed by MSK") // UserTrustResult.KeyNotSigned(mySSKey) - } - - // Check that Alice USK signature of Alice MSK is valid - try { - crossSigningOlm.olmUtility.verifyEd25519Signature( - ssKeySignaturesMadeByMyMasterKey, - myMasterKey.unpaddedBase64PublicKey, - mySSKey.canonicalSignable() - ) - } catch (failure: Throwable) { - return UserTrustResult.Failure("Invalid signature $ssKeySignaturesMadeByMyMasterKey") // UserTrustResult.InvalidSignature(mySSKey, ssKeySignaturesMadeByMyMasterKey) - } - - return UserTrustResult.Success - } - - override suspend fun getUserCrossSigningKeys(otherUserId: String): MXCrossSigningInfo? { - return withContext(coroutineDispatchers.io) { - cryptoStore.getCrossSigningInfo(otherUserId) - } - } - - override fun getLiveCrossSigningKeys(userId: String): LiveData> { - return cryptoStore.getLiveCrossSigningInfo(userId) - } - - override suspend fun getMyCrossSigningKeys(): MXCrossSigningInfo? { - return withContext(coroutineDispatchers.io) { - cryptoStore.getMyCrossSigningInfo() - } - } - - override suspend fun getCrossSigningPrivateKeys(): PrivateKeysInfo? { - return withContext(coroutineDispatchers.io) { - cryptoStore.getCrossSigningPrivateKeys() - } - } - - override fun getLiveCrossSigningPrivateKeys(): LiveData> { - return cryptoStore.getLiveCrossSigningPrivateKeys() - } - - override fun canCrossSign(): Boolean { - return checkSelfTrust().isVerified() && cryptoStore.getCrossSigningPrivateKeys()?.selfSigned != null && - cryptoStore.getCrossSigningPrivateKeys()?.user != null - } - - override fun allPrivateKeysKnown(): Boolean { - return checkSelfTrust().isVerified() && - cryptoStore.getCrossSigningPrivateKeys()?.allKnown().orFalse() - } - - override suspend fun trustUser(otherUserId: String) { - withContext(coroutineDispatchers.crypto) { - Timber.d("## CrossSigning - Mark user $otherUserId as trusted ") - // We should have this user keys - val otherMasterKeys = getUserCrossSigningKeys(otherUserId)?.masterKey() - if (otherMasterKeys == null) { - throw Throwable("## CrossSigning - Other master signing key is not known") - } - val myKeys = getUserCrossSigningKeys(myUserId) - ?: throw Throwable("## CrossSigning - CrossSigning is not setup for this account") - - val userPubKey = myKeys.userKey()?.unpaddedBase64PublicKey - if (userPubKey == null || crossSigningOlm.userPkSigning == null) { - throw Throwable("## CrossSigning - Cannot sign from this account, privateKeyUnknown $userPubKey") - } - - // Sign the other MasterKey with our UserSigning key - val newSignature = JsonCanonicalizer.getCanonicalJson( - Map::class.java, - otherMasterKeys.signalableJSONDictionary() - ).let { crossSigningOlm.userPkSigning?.sign(it) } - ?: // race?? - throw Throwable("## CrossSigning - Failed to sign") - - cryptoStore.setUserKeysAsTrusted(otherUserId, true) - - Timber.d("## CrossSigning - Upload signature of $otherUserId MSK signed by USK") - val uploadQuery = UploadSignatureQueryBuilder() - .withSigningKeyInfo(otherMasterKeys.copyForSignature(myUserId, userPubKey, newSignature)) - .build() - - uploadSignaturesTask.execute(UploadSignaturesTask.Params(uploadQuery)) - - // Local echo for device cross trust, to avoid having to wait for a notification of key change - cryptoStore.getUserDeviceList(otherUserId)?.forEach { device -> - val updatedTrust = checkDeviceTrust(device.userId, device.deviceId, device.trustLevel?.isLocallyVerified() ?: false) - Timber.v("## CrossSigning - update trust for device ${device.deviceId} of user $otherUserId , verified=$updatedTrust") - cryptoStore.setDeviceTrust(device.userId, device.deviceId, updatedTrust.isCrossSignedVerified(), updatedTrust.isLocallyVerified()) - } - } - } - - override suspend fun markMyMasterKeyAsTrusted() { - withContext(coroutineDispatchers.crypto) { - cryptoStore.markMyMasterKeyAsLocallyTrusted(true) - checkSelfTrust() - // re-verify all trusts - onUsersDeviceUpdate(listOf(myUserId)) - } - } - - override suspend fun trustDevice(deviceId: String) { - withContext(coroutineDispatchers.crypto) { - // This device should be yours - val device = cryptoStore.getUserDevice(myUserId, deviceId) - ?: throw IllegalArgumentException("This device [$deviceId] is not known, or not yours") - - val myKeys = getUserCrossSigningKeys(myUserId) - ?: throw Throwable("CrossSigning is not setup for this account") - - val ssPubKey = myKeys.selfSigningKey()?.unpaddedBase64PublicKey - if (ssPubKey == null || crossSigningOlm.selfSigningPkSigning == null) { - throw Throwable("Cannot sign from this account, public and/or privateKey Unknown $ssPubKey") - } - - // Sign with self signing - val newSignature = crossSigningOlm.selfSigningPkSigning?.sign(device.canonicalSignable()) - ?: throw Throwable("Failed to sign") - - val toUpload = device.copy( - signatures = mapOf( - myUserId - to - mapOf( - "ed25519:$ssPubKey" to newSignature - ) - ) - ) - - val uploadQuery = UploadSignatureQueryBuilder() - .withDeviceInfo(toUpload) - .build() - uploadSignaturesTask.execute(UploadSignaturesTask.Params(uploadQuery)) - } - } - - override suspend fun shieldForGroup(userIds: List): RoomEncryptionTrustLevel { - // Not used in kotlin SDK? - TODO("Not yet implemented") - } - - override suspend fun checkDeviceTrust(otherUserId: String, otherDeviceId: String, locallyTrusted: Boolean?): DeviceTrustResult { - val otherDevice = cryptoStore.getUserDevice(otherUserId, otherDeviceId) - ?: return DeviceTrustResult.UnknownDevice(otherDeviceId) - - val myKeys = getUserCrossSigningKeys(myUserId) - ?: return legacyFallbackTrust(locallyTrusted, DeviceTrustResult.CrossSigningNotConfigured(myUserId)) - - if (!myKeys.isTrusted()) return legacyFallbackTrust(locallyTrusted, DeviceTrustResult.KeysNotTrusted(myKeys)) - - val otherKeys = getUserCrossSigningKeys(otherUserId) - ?: return legacyFallbackTrust(locallyTrusted, DeviceTrustResult.CrossSigningNotConfigured(otherUserId)) - - // TODO should we force verification ? - if (!otherKeys.isTrusted()) return legacyFallbackTrust(locallyTrusted, DeviceTrustResult.KeysNotTrusted(otherKeys)) - - // Check if the trust chain is valid - /* - * ┏━━━━━━━━┓ ┏━━━━━━━━┓ - * ┃ ALICE ┃ ┃ BOB ┃ - * ┗━━━━━━━━┛ ┗━━━━━━━━┛ - * MSK ┌────────────▶MSK - * │ - * │ │ │ - * │ SSK │ └──▶ SSK ──────────────────┐ - * │ │ │ - * │ │ USK │ - * └──▶ USK ────────────┘ (not visible by │ - * Alice) │ - * ▼ - * ┌──────────────┐ - * │ BOB's Device │ - * └──────────────┘ - */ - - val otherSSKSignature = otherDevice.signatures?.get(otherUserId)?.get("ed25519:${otherKeys.selfSigningKey()?.unpaddedBase64PublicKey}") - ?: return legacyFallbackTrust( - locallyTrusted, - DeviceTrustResult.MissingDeviceSignature( - otherDeviceId, otherKeys.selfSigningKey() - ?.unpaddedBase64PublicKey - ?: "" - ) - ) - - // Check bob's device is signed by bob's SSK - try { - crossSigningOlm.olmUtility.verifyEd25519Signature( - otherSSKSignature, - otherKeys.selfSigningKey()?.unpaddedBase64PublicKey, - otherDevice.canonicalSignable() - ) - } catch (e: Throwable) { - return legacyFallbackTrust(locallyTrusted, DeviceTrustResult.InvalidDeviceSignature(otherDeviceId, otherSSKSignature, e)) - } - - return DeviceTrustResult.Success(DeviceTrustLevel(crossSigningVerified = true, locallyVerified = locallyTrusted)) - } - - fun checkDeviceTrust(myKeys: MXCrossSigningInfo?, otherKeys: MXCrossSigningInfo?, otherDevice: CryptoDeviceInfo): DeviceTrustResult { - val locallyTrusted = otherDevice.trustLevel?.isLocallyVerified() - myKeys ?: return legacyFallbackTrust(locallyTrusted, DeviceTrustResult.CrossSigningNotConfigured(myUserId)) - - if (!myKeys.isTrusted()) return legacyFallbackTrust(locallyTrusted, DeviceTrustResult.KeysNotTrusted(myKeys)) - - otherKeys ?: return legacyFallbackTrust(locallyTrusted, DeviceTrustResult.CrossSigningNotConfigured(otherDevice.userId)) - - // TODO should we force verification ? - if (!otherKeys.isTrusted()) return legacyFallbackTrust(locallyTrusted, DeviceTrustResult.KeysNotTrusted(otherKeys)) - - // Check if the trust chain is valid - /* - * ┏━━━━━━━━┓ ┏━━━━━━━━┓ - * ┃ ALICE ┃ ┃ BOB ┃ - * ┗━━━━━━━━┛ ┗━━━━━━━━┛ - * MSK ┌────────────▶MSK - * │ - * │ │ │ - * │ SSK │ └──▶ SSK ──────────────────┐ - * │ │ │ - * │ │ USK │ - * └──▶ USK ────────────┘ (not visible by │ - * Alice) │ - * ▼ - * ┌──────────────┐ - * │ BOB's Device │ - * └──────────────┘ - */ - - val otherSSKSignature = otherDevice.signatures?.get(otherKeys.userId)?.get("ed25519:${otherKeys.selfSigningKey()?.unpaddedBase64PublicKey}") - ?: return legacyFallbackTrust( - locallyTrusted, - DeviceTrustResult.MissingDeviceSignature( - otherDevice.deviceId, otherKeys.selfSigningKey() - ?.unpaddedBase64PublicKey - ?: "" - ) - ) - - // Check bob's device is signed by bob's SSK - try { - crossSigningOlm.olmUtility.verifyEd25519Signature( - otherSSKSignature, - otherKeys.selfSigningKey()?.unpaddedBase64PublicKey, - otherDevice.canonicalSignable() - ) - } catch (e: Throwable) { - return legacyFallbackTrust(locallyTrusted, DeviceTrustResult.InvalidDeviceSignature(otherDevice.deviceId, otherSSKSignature, e)) - } - - return DeviceTrustResult.Success(DeviceTrustLevel(crossSigningVerified = true, locallyVerified = locallyTrusted)) - } - - private fun legacyFallbackTrust(locallyTrusted: Boolean?, crossSignTrustFail: DeviceTrustResult): DeviceTrustResult { - return if (locallyTrusted == true) { - DeviceTrustResult.Success(DeviceTrustLevel(crossSigningVerified = false, locallyVerified = true)) - } else { - crossSignTrustFail - } - } - - override fun onUsersDeviceUpdate(userIds: List) { - Timber.d("## CrossSigning - onUsersDeviceUpdate for users: ${userIds.logLimit()}") - runBlocking { - checkTrustAndAffectedRoomShields(userIds) - } - } - - override suspend fun checkTrustAndAffectedRoomShields(userIds: List) { - Timber.d("## CrossSigning - checkTrustAndAffectedRoomShields for users: ${userIds.logLimit()}") - val workerParams = UpdateTrustWorker.Params( - sessionId = sessionId, - filename = updateTrustWorkerDataRepository.createParam(userIds) - ) - val workerData = WorkerParamsFactory.toData(workerParams) - - val workRequest = workManagerProvider.matrixOneTimeWorkRequestBuilder() - .setInputData(workerData) - .setBackoffCriteria(BackoffPolicy.LINEAR, WorkManagerProvider.BACKOFF_DELAY_MILLIS, TimeUnit.MILLISECONDS) - .build() - - workManagerProvider.workManager - .beginUniqueWork("TRUST_UPDATE_QUEUE", ExistingWorkPolicy.APPEND_OR_REPLACE, workRequest) - .enqueue() - } - - private suspend fun setUserKeysAsTrusted(otherUserId: String, trusted: Boolean) { - val currentTrust = cryptoStore.getCrossSigningInfo(otherUserId)?.isTrusted() - cryptoStore.setUserKeysAsTrusted(otherUserId, trusted) - // If it's me, recheck trust of all users and devices? - val users = ArrayList() - if (otherUserId == myUserId && currentTrust != trusted) { - // notify key requester - outgoingKeyRequestManager.onSelfCrossSigningTrustChanged(trusted) - cryptoStore.updateUsersTrust { - users.add(it) - // called within a real transaction, has to block - runBlocking { - checkUserTrust(it).isVerified() - } - } - - users.forEach { - cryptoStore.getUserDeviceList(it)?.forEach { device -> - val updatedTrust = checkDeviceTrust(it, device.deviceId, device.trustLevel?.isLocallyVerified() ?: false) - Timber.v("## CrossSigning - update trust for device ${device.deviceId} of user $otherUserId , verified=$updatedTrust") - cryptoStore.setDeviceTrust(it, device.deviceId, updatedTrust.isCrossSignedVerified(), updatedTrust.isLocallyVerified()) - } - } - } - } -} diff --git a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/crosssigning/Extensions.kt b/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/crosssigning/Extensions.kt deleted file mode 100644 index b8f1746664..0000000000 --- a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/crosssigning/Extensions.kt +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright 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. - */ -package org.matrix.android.sdk.internal.crypto.crosssigning - -import org.matrix.android.sdk.api.session.crypto.crosssigning.CryptoCrossSigningKey -import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo -import org.matrix.android.sdk.internal.util.JsonCanonicalizer - -internal fun CryptoDeviceInfo.canonicalSignable(): String { - return JsonCanonicalizer.getCanonicalJson(Map::class.java, signalableJSONDictionary()) -} - -internal fun CryptoCrossSigningKey.canonicalSignable(): String { - return JsonCanonicalizer.getCanonicalJson(Map::class.java, signalableJSONDictionary()) -} diff --git a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/crosssigning/UpdateTrustWorker.kt b/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/crosssigning/UpdateTrustWorker.kt deleted file mode 100644 index 80f37a6c57..0000000000 --- a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/crosssigning/UpdateTrustWorker.kt +++ /dev/null @@ -1,390 +0,0 @@ -/* - * Copyright (c) 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. - */ - -package org.matrix.android.sdk.internal.crypto.crosssigning - -import android.content.Context -import androidx.work.WorkerParameters -import com.squareup.moshi.JsonClass -import io.realm.Realm -import io.realm.RealmConfiguration -import io.realm.kotlin.where -import kotlinx.coroutines.runBlocking -import org.matrix.android.sdk.api.extensions.orFalse -import org.matrix.android.sdk.api.session.crypto.crosssigning.CrossSigningService -import org.matrix.android.sdk.api.session.crypto.crosssigning.MXCrossSigningInfo -import org.matrix.android.sdk.api.session.crypto.crosssigning.UserTrustResult -import org.matrix.android.sdk.api.session.crypto.crosssigning.isCrossSignedVerified -import org.matrix.android.sdk.api.session.crypto.crosssigning.isVerified -import org.matrix.android.sdk.api.session.crypto.model.RoomEncryptionTrustLevel -import org.matrix.android.sdk.internal.SessionManager -import org.matrix.android.sdk.internal.crypto.CryptoSessionInfoProvider -import org.matrix.android.sdk.internal.crypto.store.db.mapper.CrossSigningKeysMapper -import org.matrix.android.sdk.internal.crypto.store.db.model.CrossSigningInfoEntity -import org.matrix.android.sdk.internal.crypto.store.db.model.CrossSigningInfoEntityFields -import org.matrix.android.sdk.internal.crypto.store.db.model.CryptoMapper -import org.matrix.android.sdk.internal.crypto.store.db.model.TrustLevelEntity -import org.matrix.android.sdk.internal.crypto.store.db.model.UserEntity -import org.matrix.android.sdk.internal.crypto.store.db.model.UserEntityFields -import org.matrix.android.sdk.internal.database.awaitTransaction -import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity -import org.matrix.android.sdk.internal.database.query.where -import org.matrix.android.sdk.internal.di.CryptoDatabase -import org.matrix.android.sdk.internal.di.SessionDatabase -import org.matrix.android.sdk.internal.di.UserId -import org.matrix.android.sdk.internal.session.SessionComponent -import org.matrix.android.sdk.internal.session.room.membership.RoomMemberHelper -import org.matrix.android.sdk.internal.util.logLimit -import org.matrix.android.sdk.internal.worker.SessionSafeCoroutineWorker -import org.matrix.android.sdk.internal.worker.SessionWorkerParams -import timber.log.Timber -import javax.inject.Inject - -internal class UpdateTrustWorker(context: Context, params: WorkerParameters, sessionManager: SessionManager) : - SessionSafeCoroutineWorker(context, params, sessionManager, Params::class.java) { - - @JsonClass(generateAdapter = true) - internal data class Params( - override val sessionId: String, - override val lastFailureMessage: String? = null, - // Kept for compatibility, but not used anymore (can be used for pending Worker) - val updatedUserIds: List? = null, - // Passing a long list of userId can break the Work Manager due to data size limitation. - // so now we use a temporary file to store the data - val filename: String? = null - ) : SessionWorkerParams - - @Inject lateinit var crossSigningService: CrossSigningService - - // It breaks the crypto store contract, but we need to batch things :/ - @CryptoDatabase - @Inject lateinit var cryptoRealmConfiguration: RealmConfiguration - - @SessionDatabase - @Inject lateinit var sessionRealmConfiguration: RealmConfiguration - - @UserId - @Inject lateinit var myUserId: String - @Inject lateinit var crossSigningKeysMapper: CrossSigningKeysMapper - @Inject lateinit var updateTrustWorkerDataRepository: UpdateTrustWorkerDataRepository - @Inject lateinit var cryptoSessionInfoProvider: CryptoSessionInfoProvider - - // @Inject lateinit var roomSummaryUpdater: RoomSummaryUpdater -// @Inject lateinit var cryptoStore: IMXCryptoStore - - override fun injectWith(injector: SessionComponent) { - injector.inject(this) - } - - override suspend fun doSafeWork(params: Params): Result { - val sId = myUserId.take(5) - Timber.v("## CrossSigning - UpdateTrustWorker started..") - val workerParams = params.filename - ?.let { updateTrustWorkerDataRepository.getParam(it) } - ?: return Result.success().also { - Timber.w("## CrossSigning - UpdateTrustWorker failed to get params") - cleanup(params) - } - - Timber.v("## CrossSigning [$sId]- UpdateTrustWorker userIds:${workerParams.userIds.logLimit()}, roomIds:${workerParams.roomIds.orEmpty().logLimit()}") - val userList = workerParams.userIds - - // List should not be empty, but let's avoid go further in case of empty list - if (userList.isNotEmpty()) { - // Unfortunately we don't have much info on what did exactly changed (is it the cross signing keys of that user, - // or a new device?) So we check all again :/ - Timber.v("## CrossSigning [$sId]- Updating trust for users: ${userList.logLimit()}") - updateTrust(userList) - } - - val roomsToCheck = workerParams.roomIds ?: cryptoSessionInfoProvider.getRoomsWhereUsersAreParticipating(userList) - Timber.v("## CrossSigning [$sId]- UpdateTrustWorker roomShield to check:${roomsToCheck.logLimit()}") - var myCrossSigningInfo: MXCrossSigningInfo? - Realm.getInstance(cryptoRealmConfiguration).use { realm -> - myCrossSigningInfo = getCrossSigningInfo(realm, myUserId) - } - // So Cross Signing keys trust is updated, device trust is updated - // We can now update room shields? in the session DB? - updateRoomShieldInSummaries(roomsToCheck, myCrossSigningInfo) - - cleanup(params) - return Result.success() - } - - private suspend fun updateTrust(userListParam: List) { - val sId = myUserId.take(5) - var userList = userListParam - var myCrossSigningInfo: MXCrossSigningInfo? - - // First we check that the users MSK are trusted by mine - // After that we check the trust chain for each devices of each users - awaitTransaction(cryptoRealmConfiguration) { cryptoRealm -> - // By mapping here to model, this object is not live - // I should update it if needed - myCrossSigningInfo = getCrossSigningInfo(cryptoRealm, myUserId) - - var myTrustResult: UserTrustResult? = null - - if (userList.contains(myUserId)) { - Timber.d("## CrossSigning [$sId]- Clear all trust as a change on my user was detected") - // i am in the list.. but i don't know exactly the delta of change :/ - // If it's my cross signing keys we should refresh all trust - // do it anyway ? - userList = cryptoRealm.where(CrossSigningInfoEntity::class.java) - .findAll() - .mapNotNull { it.userId } - - // check right now my keys and mark it as trusted as other trust depends on it - val myDevices = cryptoRealm.where() - .equalTo(UserEntityFields.USER_ID, myUserId) - .findFirst() - ?.devices - ?.map { CryptoMapper.mapToModel(it) } - - myTrustResult = crossSigningService.checkSelfTrust(myCrossSigningInfo, myDevices) - updateCrossSigningKeysTrust(cryptoRealm, myUserId, myTrustResult.isVerified()) - // update model reference - myCrossSigningInfo = getCrossSigningInfo(cryptoRealm, myUserId) - } - - val otherInfos = userList.associateWith { userId -> - getCrossSigningInfo(cryptoRealm, userId) - } - - val trusts = otherInfos.mapValues { entry -> - when (entry.key) { - myUserId -> myTrustResult - else -> { - crossSigningService.checkOtherMSKTrusted(myCrossSigningInfo, entry.value).also { - Timber.v("## CrossSigning [$sId]- user:${entry.key} result:$it") - } - } - } - } - - // TODO! if it's me and my keys has changed... I have to reset trust for everyone! - // i have all the new trusts, update DB - trusts.forEach { - val verified = it.value?.isVerified() == true - Timber.v("[$myUserId] ## CrossSigning [$sId]- Updating user trust: ${it.key} to $verified") - updateCrossSigningKeysTrust(cryptoRealm, it.key, verified) - } - - // Ok so now we have to check device trust for all these users.. - Timber.v("## CrossSigning [$sId]- Updating devices cross trust users: ${trusts.keys.logLimit()}") - trusts.keys.forEach { userId -> - val devicesEntities = cryptoRealm.where() - .equalTo(UserEntityFields.USER_ID, userId) - .findFirst() - ?.devices - - val trustMap = devicesEntities?.associateWith { device -> - runBlocking { - crossSigningService.checkDeviceTrust(userId, device.deviceId ?: "", CryptoMapper.mapToModel(device).trustLevel?.locallyVerified) - } - } - - // Update trust if needed - devicesEntities?.forEach { device -> - val crossSignedVerified = trustMap?.get(device)?.isCrossSignedVerified() - Timber.v("## CrossSigning [$sId]- Trust for ${device.userId}|${device.deviceId} : cross verified: ${trustMap?.get(device)}") - if (device.trustLevelEntity?.crossSignedVerified != crossSignedVerified) { - Timber.d("## CrossSigning [$sId]- Trust change detected for ${device.userId}|${device.deviceId} : cross verified: $crossSignedVerified") - // need to save - val trustEntity = device.trustLevelEntity - if (trustEntity == null) { - device.trustLevelEntity = cryptoRealm.createObject(TrustLevelEntity::class.java).also { - it.locallyVerified = false - it.crossSignedVerified = crossSignedVerified - } - } else { - trustEntity.crossSignedVerified = crossSignedVerified - } - } else { - Timber.v("## CrossSigning [$sId]- Trust unchanged for ${device.userId}|${device.deviceId} : cross verified: $crossSignedVerified") - } - } - } - } - } - - private suspend fun updateRoomShieldInSummaries(roomList: List, myCrossSigningInfo: MXCrossSigningInfo?) { - val sId = myUserId.take(5) - Timber.d("## CrossSigning [$sId]- Updating shields for impacted rooms... ${roomList.logLimit()}") - awaitTransaction(sessionRealmConfiguration) { sessionRealm -> - Timber.d("## CrossSigning - Updating shields for impacted rooms - in transaction") - Realm.getInstance(cryptoRealmConfiguration).use { cryptoRealm -> - roomList.forEach { roomId -> - Timber.v("## CrossSigning [$sId]- Checking room $roomId") - RoomSummaryEntity.where(sessionRealm, roomId) -// .equalTo(RoomSummaryEntityFields.IS_ENCRYPTED, true) - .findFirst() - ?.let { roomSummary -> - Timber.v("## CrossSigning [$sId]- Check shield state for room $roomId") - val allActiveRoomMembers = RoomMemberHelper(sessionRealm, roomId).getActiveRoomMemberIds() - try { - val updatedTrust = computeRoomShield( - myCrossSigningInfo, - cryptoRealm, - allActiveRoomMembers, - roomSummary - ) - if (roomSummary.roomEncryptionTrustLevel != updatedTrust) { - Timber.d("## CrossSigning [$sId]- Shield change detected for $roomId -> $updatedTrust") - roomSummary.roomEncryptionTrustLevel = updatedTrust - } else { - Timber.v("## CrossSigning [$sId]- Shield unchanged for $roomId -> $updatedTrust") - } - } catch (failure: Throwable) { - Timber.e(failure) - } - } - } - } - } - Timber.d("## CrossSigning - Updating shields for impacted rooms - END") - } - - private fun getCrossSigningInfo(cryptoRealm: Realm, userId: String): MXCrossSigningInfo? { - return cryptoRealm.where(CrossSigningInfoEntity::class.java) - .equalTo(CrossSigningInfoEntityFields.USER_ID, userId) - .findFirst() - ?.let { mapCrossSigningInfoEntity(it) } - } - - private fun cleanup(params: Params) { - params.filename - ?.let { updateTrustWorkerDataRepository.delete(it) } - } - - private fun updateCrossSigningKeysTrust(cryptoRealm: Realm, userId: String, verified: Boolean) { - cryptoRealm.where(CrossSigningInfoEntity::class.java) - .equalTo(CrossSigningInfoEntityFields.USER_ID, userId) - .findFirst() - ?.let { userKeyInfo -> - userKeyInfo - .crossSigningKeys - .forEach { key -> - // optimization to avoid trigger updates when there is no change.. - if (key.trustLevelEntity?.isVerified() != verified) { - Timber.d("## CrossSigning - Trust change for $userId : $verified") - val level = key.trustLevelEntity - if (level == null) { - key.trustLevelEntity = cryptoRealm.createObject(TrustLevelEntity::class.java).also { - it.locallyVerified = verified - it.crossSignedVerified = verified - } - } else { - level.locallyVerified = verified - level.crossSignedVerified = verified - } - } - } - if (verified) { - userKeyInfo.wasUserVerifiedOnce = true - } - } - } - - private fun computeRoomShield( - myCrossSigningInfo: MXCrossSigningInfo?, - cryptoRealm: Realm, - activeMemberUserIds: List, - roomSummaryEntity: RoomSummaryEntity - ): RoomEncryptionTrustLevel { - Timber.v("## CrossSigning - computeRoomShield ${roomSummaryEntity.roomId} -> ${activeMemberUserIds.logLimit()}") - // The set of “all users” depends on the type of room: - // For regular / topic rooms which have more than 2 members (including yourself) are considered when decorating a room - // For 1:1 and group DM rooms, all other users (i.e. excluding yourself) are considered when decorating a room - val listToCheck = if (roomSummaryEntity.isDirect || activeMemberUserIds.size <= 2) { - activeMemberUserIds.filter { it != myUserId } - } else { - activeMemberUserIds - } - - val allTrustedUserIds = listToCheck - .filter { userId -> - getCrossSigningInfo(cryptoRealm, userId)?.isTrusted() == true - } - - val resetTrust = listToCheck - .filter { userId -> - val crossSigningInfo = getCrossSigningInfo(cryptoRealm, userId) - crossSigningInfo?.isTrusted() != true && crossSigningInfo?.wasTrustedOnce == true - } - - return if (allTrustedUserIds.isEmpty()) { - if (resetTrust.isEmpty()) { - RoomEncryptionTrustLevel.Default - } else { - RoomEncryptionTrustLevel.Warning - } - } else { - // If one of the verified user as an untrusted device -> warning - // If all devices of all verified users are trusted -> green - // else -> black - allTrustedUserIds - .mapNotNull { userId -> - cryptoRealm.where() - .equalTo(UserEntityFields.USER_ID, userId) - .findFirst() - ?.devices - ?.map { CryptoMapper.mapToModel(it) } - } - .flatten() - .let { allDevices -> - Timber.v("## CrossSigning - computeRoomShield ${roomSummaryEntity.roomId} devices ${allDevices.map { it.deviceId }.logLimit()}") - if (myCrossSigningInfo != null) { - allDevices.any { !it.trustLevel?.crossSigningVerified.orFalse() } - } else { - // Legacy method - allDevices.any { !it.isVerified } - } - } - .let { hasWarning -> - if (hasWarning) { - RoomEncryptionTrustLevel.Warning - } else { - if (resetTrust.isEmpty()) { - if (listToCheck.size == allTrustedUserIds.size) { - // all users are trusted and all devices are verified - RoomEncryptionTrustLevel.Trusted - } else { - RoomEncryptionTrustLevel.Default - } - } else { - RoomEncryptionTrustLevel.Warning - } - } - } - } - } - - private fun mapCrossSigningInfoEntity(xsignInfo: CrossSigningInfoEntity): MXCrossSigningInfo { - val userId = xsignInfo.userId ?: "" - return MXCrossSigningInfo( - userId = userId, - crossSigningKeys = xsignInfo.crossSigningKeys.mapNotNull { - crossSigningKeysMapper.map(userId, it) - }, - wasTrustedOnce = xsignInfo.wasUserVerifiedOnce - ) - } - - override fun buildErrorParams(params: Params, message: String): Params { - return params.copy(lastFailureMessage = params.lastFailureMessage ?: message) - } -} diff --git a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/keysbackup/DefaultKeysBackupService.kt b/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/keysbackup/DefaultKeysBackupService.kt deleted file mode 100644 index 078f62dd77..0000000000 --- a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/keysbackup/DefaultKeysBackupService.kt +++ /dev/null @@ -1,1337 +0,0 @@ -/* - * Copyright 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. - */ - -package org.matrix.android.sdk.internal.crypto.keysbackup - -import android.os.Handler -import android.os.Looper -import androidx.annotation.VisibleForTesting -import androidx.annotation.WorkerThread -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch -import kotlinx.coroutines.sync.withLock -import kotlinx.coroutines.withContext -import org.matrix.android.sdk.api.MatrixCallback -import org.matrix.android.sdk.api.MatrixConfiguration -import org.matrix.android.sdk.api.MatrixCoroutineDispatchers -import org.matrix.android.sdk.api.auth.data.Credentials -import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_MEGOLM_BACKUP -import org.matrix.android.sdk.api.failure.Failure -import org.matrix.android.sdk.api.failure.MatrixError -import org.matrix.android.sdk.api.listeners.ProgressListener -import org.matrix.android.sdk.api.listeners.StepProgressListener -import org.matrix.android.sdk.api.session.crypto.keysbackup.BackupRecoveryKey -import org.matrix.android.sdk.api.session.crypto.keysbackup.BackupUtils -import org.matrix.android.sdk.api.session.crypto.keysbackup.IBackupRecoveryKey -import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupLastVersionResult -import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupService -import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupState -import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupStateListener -import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupVersionTrust -import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupVersionTrustSignature -import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysVersion -import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysVersionResult -import org.matrix.android.sdk.api.session.crypto.keysbackup.MegolmBackupAuthData -import org.matrix.android.sdk.api.session.crypto.keysbackup.MegolmBackupCreationInfo -import org.matrix.android.sdk.api.session.crypto.keysbackup.SavedKeyBackupKeyInfo -import org.matrix.android.sdk.api.session.crypto.keysbackup.computeRecoveryKey -import org.matrix.android.sdk.api.session.crypto.keysbackup.extractCurveKeyFromRecoveryKey -import org.matrix.android.sdk.api.session.crypto.keysbackup.toKeysVersionResult -import org.matrix.android.sdk.api.session.crypto.model.ImportRoomKeysResult -import org.matrix.android.sdk.api.util.fromBase64 -import org.matrix.android.sdk.internal.crypto.InboundGroupSessionStore -import org.matrix.android.sdk.internal.crypto.MXOlmDevice -import org.matrix.android.sdk.internal.crypto.MegolmSessionData -import org.matrix.android.sdk.internal.crypto.ObjectSigner -import org.matrix.android.sdk.internal.crypto.actions.MegolmSessionDataImporter -import org.matrix.android.sdk.internal.crypto.crosssigning.CrossSigningOlm -import org.matrix.android.sdk.internal.crypto.keysbackup.model.SignalableMegolmBackupAuthData -import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.CreateKeysBackupVersionBody -import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.KeyBackupData -import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.KeysBackupData -import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.RoomKeysBackupData -import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.UpdateKeysBackupVersionBody -import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.CreateKeysBackupVersionTask -import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.DeleteBackupTask -import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.GetKeysBackupLastVersionTask -import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.GetKeysBackupVersionTask -import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.GetRoomSessionDataTask -import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.GetRoomSessionsDataTask -import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.GetSessionsDataTask -import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.StoreSessionsDataTask -import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.UpdateKeysBackupVersionTask -import org.matrix.android.sdk.internal.crypto.model.MXInboundMegolmSessionWrapper -import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore -import org.matrix.android.sdk.internal.crypto.store.db.model.KeysBackupDataEntity -import org.matrix.android.sdk.internal.crypto.tools.withOlmDecryption -import org.matrix.android.sdk.internal.di.MoshiProvider -import org.matrix.android.sdk.internal.di.UserId -import org.matrix.android.sdk.internal.session.SessionScope -import org.matrix.android.sdk.internal.task.TaskExecutor -import org.matrix.android.sdk.internal.task.configureWith -import org.matrix.android.sdk.internal.util.JsonCanonicalizer -import org.matrix.olm.OlmException -import org.matrix.olm.OlmPkDecryption -import org.matrix.olm.OlmPkEncryption -import timber.log.Timber -import java.security.InvalidParameterException -import javax.inject.Inject -import kotlin.random.Random - -/** - * A DefaultKeysBackupService class instance manage incremental backup of e2e keys (megolm keys) - * to the user's homeserver. - */ -@SessionScope -internal class DefaultKeysBackupService @Inject constructor( - @UserId private val userId: String, - private val credentials: Credentials, - private val cryptoStore: IMXCryptoStore, - private val olmDevice: MXOlmDevice, - private val objectSigner: ObjectSigner, - private val crossSigningOlm: CrossSigningOlm, - // Actions - private val megolmSessionDataImporter: MegolmSessionDataImporter, - // Tasks - private val createKeysBackupVersionTask: CreateKeysBackupVersionTask, - private val deleteBackupTask: DeleteBackupTask, - private val getKeysBackupLastVersionTask: GetKeysBackupLastVersionTask, - private val getKeysBackupVersionTask: GetKeysBackupVersionTask, - private val getRoomSessionDataTask: GetRoomSessionDataTask, - private val getRoomSessionsDataTask: GetRoomSessionsDataTask, - private val getSessionsDataTask: GetSessionsDataTask, - private val storeSessionDataTask: StoreSessionsDataTask, - private val updateKeysBackupVersionTask: UpdateKeysBackupVersionTask, - // Task executor - private val taskExecutor: TaskExecutor, - private val matrixConfiguration: MatrixConfiguration, - private val inboundGroupSessionStore: InboundGroupSessionStore, - private val coroutineDispatchers: MatrixCoroutineDispatchers, - private val cryptoCoroutineScope: CoroutineScope -) : KeysBackupService { - - private val uiHandler = Handler(Looper.getMainLooper()) - - private val keysBackupStateManager = KeysBackupStateManager(uiHandler) - - // The backup version - override var keysBackupVersion: KeysVersionResult? = null - private set - - // The backup key being used. - private var backupOlmPkEncryption: OlmPkEncryption? = null - - private var backupAllGroupSessionsCallback: MatrixCallback? = null - - private var keysBackupStateListener: KeysBackupStateListener? = null - - override fun isEnabled(): Boolean = keysBackupStateManager.isEnabled - - override fun isStuck(): Boolean = keysBackupStateManager.isStuck - - override fun getState(): KeysBackupState = keysBackupStateManager.state - - override fun addListener(listener: KeysBackupStateListener) { - keysBackupStateManager.addListener(listener) - } - - override fun removeListener(listener: KeysBackupStateListener) { - keysBackupStateManager.removeListener(listener) - } - - override suspend fun prepareKeysBackupVersion( - password: String?, - progressListener: ProgressListener?, - ): MegolmBackupCreationInfo { - var privateKey = ByteArray(0) - val signalableMegolmBackupAuthData = if (password != null) { - // Generate a private key from the password - val generatePrivateKeyResult = withContext(coroutineDispatchers.io) { - generatePrivateKeyWithPassword(password, progressListener) - } - privateKey = generatePrivateKeyResult.privateKey - val publicKey = withOlmDecryption { - it.setPrivateKey(privateKey) - } - SignalableMegolmBackupAuthData( - publicKey = publicKey, - privateKeySalt = generatePrivateKeyResult.salt, - privateKeyIterations = generatePrivateKeyResult.iterations - ) - } else { - val publicKey = withOlmDecryption { pkDecryption -> - pkDecryption.generateKey().also { - privateKey = pkDecryption.privateKey() - } - } - SignalableMegolmBackupAuthData(publicKey = publicKey) - } - - val canonicalJson = JsonCanonicalizer.getCanonicalJson(Map::class.java, signalableMegolmBackupAuthData.signalableJSONDictionary()) - - val signatures = mutableMapOf>() - - val deviceSignature = objectSigner.signObject(canonicalJson) - deviceSignature.forEach { (userID, content) -> - signatures[userID] = content.toMutableMap() - } - - try { - val crossSign = crossSigningOlm.signObject(CrossSigningOlm.KeyType.MASTER, canonicalJson) - signatures[credentials.userId]?.putAll(crossSign) - } catch (failure: Throwable) { - // ignore and log - Timber.w(failure, "prepareKeysBackupVersion: failed to sign with cross signing keys") - } - - val signedMegolmBackupAuthData = MegolmBackupAuthData( - publicKey = signalableMegolmBackupAuthData.publicKey, - privateKeySalt = signalableMegolmBackupAuthData.privateKeySalt, - privateKeyIterations = signalableMegolmBackupAuthData.privateKeyIterations, - signatures = signatures - ) - - return MegolmBackupCreationInfo( - algorithm = MXCRYPTO_ALGORITHM_MEGOLM_BACKUP, - authData = signedMegolmBackupAuthData, - recoveryKey = BackupRecoveryKey( - key = privateKey - ) - ) - } - - override suspend fun createKeysBackupVersion( - keysBackupCreationInfo: MegolmBackupCreationInfo, - ): KeysVersion { - @Suppress("UNCHECKED_CAST") - val createKeysBackupVersionBody = CreateKeysBackupVersionBody( - algorithm = keysBackupCreationInfo.algorithm, - authData = keysBackupCreationInfo.authData.toJsonDict() - ) - - keysBackupStateManager.state = KeysBackupState.Enabling - - try { - val data = createKeysBackupVersionTask.executeRetry(createKeysBackupVersionBody, 3) - - withContext(coroutineDispatchers.crypto) { - cryptoStore.resetBackupMarkers() - val keyBackupVersion = KeysVersionResult( - algorithm = createKeysBackupVersionBody.algorithm, - authData = createKeysBackupVersionBody.authData, - version = data.version, - // We can consider that the server does not have keys yet - count = 0, - hash = "" - ) - enableKeysBackup(keyBackupVersion) - } - - return data - } catch (failure: Throwable) { - keysBackupStateManager.state = KeysBackupState.Disabled - throw failure - } - } - - override suspend fun deleteBackup(version: String) { - // If we're currently backing up to this backup... stop. - // (We start using it automatically in createKeysBackupVersion so this is symmetrical). - if (keysBackupVersion != null && version == keysBackupVersion?.version) { - resetKeysBackupData() - keysBackupVersion = null - keysBackupStateManager.state = KeysBackupState.Unknown - } - - deleteBackupTask.executeRetry(DeleteBackupTask.Params(version), 3) - if (getState() == KeysBackupState.Unknown) { - checkAndStartKeysBackup() - } - } - - override suspend fun canRestoreKeys(): Boolean { - // Server contains more keys than locally - val totalNumberOfKeysLocally = getTotalNumbersOfKeys() - - val keysBackupData = cryptoStore.getKeysBackupData() - - val totalNumberOfKeysServer = keysBackupData?.backupLastServerNumberOfKeys ?: -1 - // Not used for the moment - // val hashServer = keysBackupData?.backupLastServerHash - - return when { - totalNumberOfKeysLocally < totalNumberOfKeysServer -> { - // Server contains more keys than this device - true - } - totalNumberOfKeysLocally == totalNumberOfKeysServer -> { - // Same number, compare hash? - // TODO We have not found any algorithm to determine if a restore is recommended here. Return false for the moment - false - } - else -> false - } - } - - override suspend fun getTotalNumbersOfKeys(): Int { - return cryptoStore.inboundGroupSessionsCount(false) - } - - override suspend fun getTotalNumbersOfBackedUpKeys(): Int { - return cryptoStore.inboundGroupSessionsCount(true) - } - -// override suspend fun backupAllGroupSessions( -// progressListener: ProgressListener?, -// ) { -// if (!isEnabled() || backupOlmPkEncryption == null || keysBackupVersion == null) { -// throw Throwable("Backup not enabled") -// } -// // Get a status right now -// getBackupProgress(object : ProgressListener { -// override fun onProgress(progress: Int, total: Int) { -// // Reset previous listeners if any -// resetBackupAllGroupSessionsListeners() -// Timber.v("backupAllGroupSessions: backupProgress: $progress/$total") -// try { -// progressListener?.onProgress(progress, total) -// } catch (e: Exception) { -// Timber.e(e, "backupAllGroupSessions: onProgress failure") -// } -// -// if (progress == total) { -// Timber.v("backupAllGroupSessions: complete") -// return -// } -// -// backupAllGroupSessionsCallback = callback -// -// // Listen to `state` change to determine when to call onBackupProgress and onComplete -// keysBackupStateListener = object : KeysBackupStateListener { -// override fun onStateChange(newState: KeysBackupState) { -// getBackupProgress(object : ProgressListener { -// override fun onProgress(progress: Int, total: Int) { -// try { -// progressListener?.onProgress(progress, total) -// } catch (e: Exception) { -// Timber.e(e, "backupAllGroupSessions: onProgress failure 2") -// } -// -// // If backup is finished, notify the main listener -// if (getState() === KeysBackupState.ReadyToBackUp) { -// backupAllGroupSessionsCallback?.onSuccess(Unit) -// resetBackupAllGroupSessionsListeners() -// } -// } -// }) -// } -// }.also { keysBackupStateManager.addListener(it) } -// -// backupKeys() -// } -// }) -// } - - override suspend fun getKeysBackupTrust( - keysBackupVersion: KeysVersionResult, - ): KeysBackupVersionTrust { - val authData = keysBackupVersion.getAuthDataAsMegolmBackupAuthData() - - if (authData == null || authData.publicKey.isEmpty() || authData.signatures.isNullOrEmpty()) { - Timber.v("getKeysBackupTrust: Key backup is absent or missing required data") - return KeysBackupVersionTrust(usable = false) - } - - val mySigs = authData.signatures[userId] - if (mySigs.isNullOrEmpty()) { - Timber.v("getKeysBackupTrust: Ignoring key backup because it lacks any signatures from this user") - return KeysBackupVersionTrust(usable = false) - } - - var keysBackupVersionTrustIsUsable = false - val keysBackupVersionTrustSignatures = mutableListOf() - - for ((keyId, mySignature) in mySigs) { - // XXX: is this how we're supposed to get the device id? - var deviceOrCrossSigningKeyId: String? = null - val components = keyId.split(":") - if (components.size == 2) { - deviceOrCrossSigningKeyId = components[1] - } - - // Let's check if it's my master key - val myMSKPKey = cryptoStore.getMyCrossSigningInfo()?.masterKey()?.unpaddedBase64PublicKey - if (deviceOrCrossSigningKeyId == myMSKPKey) { - // we have to check if we can trust - - var isSignatureValid = false - try { - crossSigningOlm.verifySignature(CrossSigningOlm.KeyType.MASTER, authData.signalableJSONDictionary(), authData.signatures) - isSignatureValid = true - } catch (failure: Throwable) { - Timber.w(failure, "getKeysBackupTrust: Bad signature from my user MSK") - } - val mskTrusted = cryptoStore.getMyCrossSigningInfo()?.masterKey()?.trustLevel?.isVerified() == true - if (isSignatureValid && mskTrusted) { - keysBackupVersionTrustIsUsable = true - } - val signature = KeysBackupVersionTrustSignature.UserSignature( - keyId = deviceOrCrossSigningKeyId, - cryptoCrossSigningKey = cryptoStore.getMyCrossSigningInfo()?.masterKey(), - valid = isSignatureValid - ) - - keysBackupVersionTrustSignatures.add(signature) - } else if (deviceOrCrossSigningKeyId != null) { - val device = cryptoStore.getUserDevice(userId, deviceOrCrossSigningKeyId) - var isSignatureValid = false - - if (device == null) { - Timber.v("getKeysBackupTrust: Signature from unknown device $deviceOrCrossSigningKeyId") - } else { - val fingerprint = device.fingerprint() - if (fingerprint != null) { - try { - olmDevice.verifySignature(fingerprint, authData.signalableJSONDictionary(), mySignature) - isSignatureValid = true - } catch (e: OlmException) { - Timber.w(e, "getKeysBackupTrust: Bad signature from device ${device.deviceId}") - } - } - - if (isSignatureValid && device.isVerified) { - keysBackupVersionTrustIsUsable = true - } - } - - val signature = KeysBackupVersionTrustSignature.DeviceSignature( - deviceId = deviceOrCrossSigningKeyId, - device = device, - valid = isSignatureValid, - ) - keysBackupVersionTrustSignatures.add(signature) - } - } - - return KeysBackupVersionTrust( - usable = keysBackupVersionTrustIsUsable, - signatures = keysBackupVersionTrustSignatures - ) - } - - override suspend fun trustKeysBackupVersion( - keysBackupVersion: KeysVersionResult, - trust: Boolean, - ) { - Timber.v("trustKeyBackupVersion: $trust, version ${keysBackupVersion.version}") - - // Get auth data to update it - val authData = getMegolmBackupAuthData(keysBackupVersion) - - if (authData == null) { - Timber.w("trustKeyBackupVersion:trust: Key backup is missing required data") - throw IllegalArgumentException("Missing element") - } else { - val updateKeysBackupVersionBody = withContext(coroutineDispatchers.crypto) { - // Get current signatures, or create an empty set - val myUserSignatures = authData.signatures?.get(userId).orEmpty().toMutableMap() - - if (trust) { - // Add current device signature - val canonicalJson = JsonCanonicalizer.getCanonicalJson(Map::class.java, authData.signalableJSONDictionary()) - - val deviceSignatures = objectSigner.signObject(canonicalJson) - - deviceSignatures[userId]?.forEach { entry -> - myUserSignatures[entry.key] = entry.value - } - } else { - // Remove current device signature - myUserSignatures.remove("ed25519:${credentials.deviceId}") - } - - // Create an updated version of KeysVersionResult - val newMegolmBackupAuthData = authData.copy() - - val newSignatures = newMegolmBackupAuthData.signatures.orEmpty().toMutableMap() - newSignatures[userId] = myUserSignatures - - val newMegolmBackupAuthDataWithNewSignature = newMegolmBackupAuthData.copy( - signatures = newSignatures - ) - - @Suppress("UNCHECKED_CAST") - UpdateKeysBackupVersionBody( - algorithm = keysBackupVersion.algorithm, - authData = newMegolmBackupAuthDataWithNewSignature.toJsonDict(), - version = keysBackupVersion.version - ) - } - - // And send it to the homeserver - updateKeysBackupVersionTask - .executeRetry(UpdateKeysBackupVersionTask.Params(keysBackupVersion.version, updateKeysBackupVersionBody), 3) - // Relaunch the state machine on this updated backup version - val newKeysBackupVersion = KeysVersionResult( - algorithm = keysBackupVersion.algorithm, - authData = updateKeysBackupVersionBody.authData, - version = keysBackupVersion.version, - hash = keysBackupVersion.hash, - count = keysBackupVersion.count - ) - - checkAndStartWithKeysBackupVersion(newKeysBackupVersion) - } - } - - override suspend fun trustKeysBackupVersionWithRecoveryKey( - keysBackupVersion: KeysVersionResult, - recoveryKey: IBackupRecoveryKey, - ) { - Timber.v("trustKeysBackupVersionWithRecoveryKey: version ${keysBackupVersion.version}") - - val isValid = isValidRecoveryKeyForKeysBackupVersion(recoveryKey, keysBackupVersion) - - if (!isValid) { - Timber.w("trustKeyBackupVersionWithRecoveryKey: Invalid recovery key.") - throw IllegalArgumentException("Invalid recovery key or password") - } else { - trustKeysBackupVersion(keysBackupVersion, true) - } - } - - override suspend fun trustKeysBackupVersionWithPassphrase( - keysBackupVersion: KeysVersionResult, - password: String, - ) { - Timber.v("trustKeysBackupVersionWithPassphrase: version ${keysBackupVersion.version}") - - val recoveryKey = recoveryKeyFromPassword(password, keysBackupVersion, null) - - if (recoveryKey == null) { - Timber.w("trustKeysBackupVersionWithPassphrase: Key backup is missing required data") - throw IllegalArgumentException("Missing element") - } else { - // Check trust using the recovery key - BackupUtils.recoveryKeyFromBase58(recoveryKey)?.let { - trustKeysBackupVersionWithRecoveryKey(keysBackupVersion, it) - } - } - } - - override suspend fun onSecretKeyGossip(secret: String) { - Timber.i("## CrossSigning - onSecretKeyGossip") - try { - val keysBackupVersion = getKeysBackupLastVersionTask.execute(Unit).toKeysVersionResult() - ?: return Unit.also { - Timber.d("Failed to get backup last version") - } - val recoveryKey = computeRecoveryKey(secret.fromBase64()).let { - BackupUtils.recoveryKeyFromBase58(it) - } ?: return Unit.also { - Timber.i("onSecretKeyGossip: Malformed key") - } - if (isValidRecoveryKeyForKeysBackupVersion(recoveryKey, keysBackupVersion)) { - // we don't want to start immediately downloading all as it can take very long - withContext(coroutineDispatchers.crypto) { - cryptoStore.saveBackupRecoveryKey(recoveryKey.toBase58(), keysBackupVersion.version) - } - Timber.i("onSecretKeyGossip: saved valid backup key") - } else { - Timber.e("onSecretKeyGossip: Recovery key is not valid ${keysBackupVersion.version}") - } - } catch (failure: Throwable) { - Timber.e("onSecretKeyGossip: failed to trust key backup version ${keysBackupVersion?.version}") - } - } - -// /** -// * Get public key from a Recovery key. -// * -// * @param recoveryKey the recovery key -// * @return the corresponding public key, from Olm -// */ -// @WorkerThread -// private fun pkPublicKeyFromRecoveryKey(recoveryKey: String): String? { -// // Extract the primary key -// val privateKey = extractCurveKeyFromRecoveryKey(recoveryKey) -// -// if (privateKey == null) { -// Timber.w("pkPublicKeyFromRecoveryKey: private key is null") -// -// return null -// } -// -// // Built the PK decryption with it -// val pkPublicKey: String -// -// try { -// val decryption = OlmPkDecryption() -// pkPublicKey = decryption.setPrivateKey(privateKey) -// } catch (e: OlmException) { -// return null -// } -// -// return pkPublicKey -// } - - private fun resetBackupAllGroupSessionsListeners() { - backupAllGroupSessionsCallback = null - - keysBackupStateListener?.let { - keysBackupStateManager.removeListener(it) - } - - keysBackupStateListener = null - } - - override suspend fun getBackupProgress(progressListener: ProgressListener) { - val backedUpKeys = cryptoStore.inboundGroupSessionsCount(true) - val total = cryptoStore.inboundGroupSessionsCount(false) - - progressListener.onProgress(backedUpKeys, total) - } - - override suspend fun restoreKeysWithRecoveryKey( - keysVersionResult: KeysVersionResult, - recoveryKey: IBackupRecoveryKey, - roomId: String?, - sessionId: String?, - stepProgressListener: StepProgressListener?, - ): ImportRoomKeysResult { - Timber.v("restoreKeysWithRecoveryKey: From backup version: ${keysVersionResult.version}") - // Check if the recovery is valid before going any further - if (!isValidRecoveryKeyForKeysBackupVersion(recoveryKey, keysVersionResult)) { - Timber.e("restoreKeysWithRecoveryKey: Invalid recovery key for this keys version") - throw InvalidParameterException("Invalid recovery key") - } - - // Save for next time and for gossiping - // Save now as it's valid, don't wait for the import as it could take long. - saveBackupRecoveryKey(recoveryKey, keysVersionResult.version) - - stepProgressListener?.onStepProgress(StepProgressListener.Step.DownloadingKey) - - // Get backed up keys from the homeserver - val data = getKeys(sessionId, roomId, keysVersionResult.version) - - return withContext(coroutineDispatchers.computation) { - val sessionsData = ArrayList() - // Restore that data - var sessionsFromHsCount = 0 - for ((roomIdLoop, backupData) in data.roomIdToRoomKeysBackupData) { - for ((sessionIdLoop, keyBackupData) in backupData.sessionIdToKeyBackupData) { - sessionsFromHsCount++ - - val sessionData = decryptKeyBackupData(keyBackupData, sessionIdLoop, roomIdLoop, recoveryKey) - - sessionData?.let { - sessionsData.add(it) - } - } - } - Timber.v( - "restoreKeysWithRecoveryKey: Decrypted ${sessionsData.size} keys out" + - " of $sessionsFromHsCount from the backup store on the homeserver" - ) - - // Do not trigger a backup for them if they come from the backup version we are using - val backUp = keysVersionResult.version != keysBackupVersion?.version - if (backUp) { - Timber.v( - "restoreKeysWithRecoveryKey: Those keys will be backed up" + - " to backup version: ${keysBackupVersion?.version}" - ) - } - - // Import them into the crypto store - val progressListener = if (stepProgressListener != null) { - object : ProgressListener { - override fun onProgress(progress: Int, total: Int) { - // Note: no need to post to UI thread, importMegolmSessionsData() will do it - stepProgressListener.onStepProgress(StepProgressListener.Step.ImportingKey(progress, total)) - } - } - } else { - null - } - - val result = megolmSessionDataImporter.handle(sessionsData, !backUp, progressListener) - - // Do not back up the key if it comes from a backup recovery - if (backUp) { - maybeBackupKeys() - } - result - } - } - - override suspend fun restoreKeyBackupWithPassword( - keysBackupVersion: KeysVersionResult, - password: String, - roomId: String?, - sessionId: String?, - stepProgressListener: StepProgressListener?, - ): ImportRoomKeysResult { - Timber.v("[MXKeyBackup] restoreKeyBackup with password: From backup version: ${keysBackupVersion.version}") - val progressListener = if (stepProgressListener != null) { - object : ProgressListener { - override fun onProgress(progress: Int, total: Int) { - uiHandler.post { - stepProgressListener.onStepProgress(StepProgressListener.Step.ComputingKey(progress, total)) - } - } - } - } else { - null - } - val recoveryKey = withContext(coroutineDispatchers.computation) { - recoveryKeyFromPassword(password, keysBackupVersion, progressListener) - }?.let { - BackupUtils.recoveryKeyFromBase58(it) - } - if (recoveryKey == null) { - Timber.v("backupKeys: Invalid configuration") - throw IllegalStateException("Invalid configuration") - } else { - return restoreKeysWithRecoveryKey(keysBackupVersion, recoveryKey, roomId, sessionId, stepProgressListener) - } - } - - /** - * Same method as [RoomKeysRestClient.getRoomKey] except that it accepts nullable - * parameters and always returns a KeysBackupData object through the Callback. - */ - private suspend fun getKeys( - sessionId: String?, - roomId: String?, - version: String - ): KeysBackupData { - return if (roomId != null && sessionId != null) { - // Get key for the room and for the session - val data = getRoomSessionDataTask.execute(GetRoomSessionDataTask.Params(roomId, sessionId, version)) - // Convert to KeysBackupData - KeysBackupData( - mutableMapOf( - roomId to RoomKeysBackupData( - mutableMapOf( - sessionId to data - ) - ) - ) - ) - } else if (roomId != null) { - // Get all keys for the room - val data = withContext(coroutineDispatchers.io) { - getRoomSessionsDataTask.execute(GetRoomSessionsDataTask.Params(roomId, version)) - } - // Convert to KeysBackupData - KeysBackupData(mutableMapOf(roomId to data)) - } else { - // Get all keys - withContext(coroutineDispatchers.io) { - getSessionsDataTask.execute(GetSessionsDataTask.Params(version)) - } - } - } - - @VisibleForTesting - @WorkerThread - fun pkDecryptionFromRecoveryKey(recoveryKey: String): OlmPkDecryption? { - // Extract the primary key - val privateKey = extractCurveKeyFromRecoveryKey(recoveryKey) - - // Built the PK decryption with it - var decryption: OlmPkDecryption? = null - if (privateKey != null) { - try { - decryption = OlmPkDecryption() - decryption.setPrivateKey(privateKey) - } catch (e: OlmException) { - Timber.e(e, "OlmException") - } - } - - return decryption - } - - /** - * Do a backup if there are new keys, with a delay. - */ - suspend fun maybeBackupKeys() { - when { - isStuck() -> { - // If not already done, or in error case, check for a valid backup version on the homeserver. - // If there is one, maybeBackupKeys will be called again. - checkAndStartKeysBackup() - } - getState() == KeysBackupState.ReadyToBackUp -> { - keysBackupStateManager.state = KeysBackupState.WillBackUp - - // Wait between 0 and 10 seconds, to avoid backup requests from - // different clients hitting the server all at the same time when a - // new key is sent - val delayInMs = Random.nextLong(KEY_BACKUP_WAITING_TIME_TO_SEND_KEY_BACKUP_MILLIS) - - cryptoCoroutineScope.launch { - delay(delayInMs) - backupKeys() - } - } - else -> { - Timber.v("maybeBackupKeys: Skip it because state: ${getState()}") - } - } - } - - override suspend fun getVersion(version: String): KeysVersionResult? { - try { - return getKeysBackupVersionTask.execute(version) - } catch (failure: Throwable) { - if (failure is Failure.ServerError && - failure.error.code == MatrixError.M_NOT_FOUND) { - // Workaround because the homeserver currently returns M_NOT_FOUND when there is no key backup - return null - } else { - // Transmit the error - throw failure - } - } - } - - override suspend fun getCurrentVersion(): KeysBackupLastVersionResult { - return getKeysBackupLastVersionTask.execute(Unit) - } - - override suspend fun forceUsingLastVersion(): Boolean { - val data = getCurrentVersion() - val localBackupVersion = keysBackupVersion?.version - when (data) { - KeysBackupLastVersionResult.NoKeysBackup -> { - if (localBackupVersion == null) { - // No backup on the server, and backup is not active - return true - } else { - // No backup on the server, and we are currently backing up, so stop backing up - return false.also { - resetKeysBackupData() - keysBackupVersion = null - keysBackupStateManager.state = KeysBackupState.Disabled - } - } - } - is KeysBackupLastVersionResult.KeysBackup -> { - if (localBackupVersion == null) { - // backup on the server, and backup is not active - return false.also { - // Do a check - checkAndStartWithKeysBackupVersion(data.keysVersionResult) - } - } else { - // Backup on the server, and we are currently backing up, compare version - if (localBackupVersion == data.keysVersionResult.version) { - // We are already using the last version of the backup - return true - } else { - // We are not using the last version, so delete the current version we are using on the server - return false.also { - // This will automatically check for the last version then - deleteBackup(localBackupVersion) - } - } - } - } - } - } - - override suspend fun checkAndStartKeysBackup() { - if (!isStuck()) { - // Try to start or restart the backup only if it is in unknown or bad state - Timber.w("checkAndStartKeysBackup: invalid state: ${getState()}") - return - } - - keysBackupVersion = null - keysBackupStateManager.state = KeysBackupState.CheckingBackUpOnHomeserver - - try { - val data = getCurrentVersion() - checkAndStartWithKeysBackupVersion(data.toKeysVersionResult()) - } catch (failure: Throwable) { - Timber.e(failure, "checkAndStartKeysBackup: Failed to get current version") - keysBackupStateManager.state = KeysBackupState.Unknown - } - } - - private suspend fun checkAndStartWithKeysBackupVersion(keyBackupVersion: KeysVersionResult?) { - Timber.v("checkAndStartWithKeyBackupVersion: ${keyBackupVersion?.version}") - - keysBackupVersion = keyBackupVersion - - if (keyBackupVersion == null) { - Timber.v("checkAndStartWithKeysBackupVersion: Found no key backup version on the homeserver") - resetKeysBackupData() - keysBackupStateManager.state = KeysBackupState.Disabled - } else { - val data = getKeysBackupTrust(keyBackupVersion) // , object : MatrixCallback { - val versionInStore = cryptoStore.getKeyBackupVersion() - - if (data.usable) { - Timber.v("checkAndStartWithKeysBackupVersion: Found usable key backup. version: ${keyBackupVersion.version}") - // Check the version we used at the previous app run - if (versionInStore != null && versionInStore != keyBackupVersion.version) { - Timber.v(" -> clean the previously used version $versionInStore") - resetKeysBackupData() - } - - Timber.v(" -> enabling key backups") - enableKeysBackup(keyBackupVersion) - } else { - Timber.v("checkAndStartWithKeysBackupVersion: No usable key backup. version: ${keyBackupVersion.version}") - if (versionInStore != null) { - Timber.v(" -> disabling key backup") - resetKeysBackupData() - } - - keysBackupStateManager.state = KeysBackupState.NotTrusted - } - } - } - -/* ========================================================================================== - * Private - * ========================================================================================== */ - - /** - * Extract MegolmBackupAuthData data from a backup version. - * - * @param keysBackupData the key backup data - * - * @return the authentication if found and valid, null in other case - */ - private fun getMegolmBackupAuthData(keysBackupData: KeysVersionResult): MegolmBackupAuthData? { - return keysBackupData - .takeIf { it.version.isNotEmpty() && it.algorithm == MXCRYPTO_ALGORITHM_MEGOLM_BACKUP } - ?.getAuthDataAsMegolmBackupAuthData() - ?.takeIf { it.publicKey.isNotEmpty() } - } - - /** - * Compute the recovery key from a password and key backup version. - * - * @param password the password. - * @param keysBackupData the backup and its auth data. - * @param progressListener listener to track progress - * - * @return the recovery key if successful, null in other cases - */ - @WorkerThread - private fun recoveryKeyFromPassword(password: String, keysBackupData: KeysVersionResult, progressListener: ProgressListener?): String? { - val authData = getMegolmBackupAuthData(keysBackupData) - - if (authData == null) { - Timber.w("recoveryKeyFromPassword: invalid parameter") - return null - } - - if (authData.privateKeySalt.isNullOrBlank() || - authData.privateKeyIterations == null) { - Timber.w("recoveryKeyFromPassword: Salt and/or iterations not found in key backup auth data") - - return null - } - - // Extract the recovery key from the passphrase - val data = retrievePrivateKeyWithPassword(password, authData.privateKeySalt, authData.privateKeyIterations, progressListener) - - return computeRecoveryKey(data) - } - - override suspend fun isValidRecoveryKeyForCurrentVersion(recoveryKey: IBackupRecoveryKey): Boolean { - // Build PK decryption instance with the recovery key - return isValidRecoveryKeyForKeysBackupVersion(recoveryKey, this.keysBackupVersion) - } - - fun isValidRecoveryKeyForKeysBackupVersion(recoveryKey: IBackupRecoveryKey, version: KeysVersionResult?): Boolean { - val megolmV1PublicKey = recoveryKey.megolmV1PublicKey() - val keysBackupData = version ?: return false - val authData = getMegolmBackupAuthData(keysBackupData) - - if (authData == null) { - Timber.w("isValidRecoveryKeyForKeysBackupVersion: Key backup is missing required data") - return false - } - - // Compare both - if (megolmV1PublicKey.publicKey != authData.publicKey) { - Timber.w("isValidRecoveryKeyForKeysBackupVersion: Public keys mismatch") - return false - } - - // Public keys match! - return true - } - - override fun computePrivateKey( - passphrase: String, - privateKeySalt: String, - privateKeyIterations: Int, - progressListener: ProgressListener - ): ByteArray { - return deriveKey(passphrase, privateKeySalt, privateKeyIterations, progressListener) - } - - /** - * Enable backing up of keys. - * This method will update the state and will start sending keys in nominal case - * - * @param keysVersionResult backup information object as returned by [getCurrentVersion]. - */ - private suspend fun enableKeysBackup(keysVersionResult: KeysVersionResult) { - val retrievedMegolmBackupAuthData = keysVersionResult.getAuthDataAsMegolmBackupAuthData() - - if (retrievedMegolmBackupAuthData != null) { - keysBackupVersion = keysVersionResult - cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { - cryptoStore.setKeyBackupVersion(keysVersionResult.version) - } - - onServerDataRetrieved(keysVersionResult.count, keysVersionResult.hash) - - try { - backupOlmPkEncryption = OlmPkEncryption().apply { - setRecipientKey(retrievedMegolmBackupAuthData.publicKey) - } - } catch (e: OlmException) { - Timber.e(e, "OlmException") - keysBackupStateManager.state = KeysBackupState.Disabled - return - } - - keysBackupStateManager.state = KeysBackupState.ReadyToBackUp - - maybeBackupKeys() - } else { - Timber.e("Invalid authentication data") - keysBackupStateManager.state = KeysBackupState.Disabled - } - } - - /** - * Update the DB with data fetch from the server. - */ - private fun onServerDataRetrieved(count: Int?, etag: String?) { - cryptoStore.setKeysBackupData(KeysBackupDataEntity() - .apply { - backupLastServerNumberOfKeys = count - backupLastServerHash = etag - } - ) - } - - /** - * Reset all local key backup data. - * - * Note: This method does not update the state - */ - private fun resetKeysBackupData() { - resetBackupAllGroupSessionsListeners() - - cryptoStore.setKeyBackupVersion(null) - cryptoStore.setKeysBackupData(null) - backupOlmPkEncryption?.releaseEncryption() - backupOlmPkEncryption = null - - // Reset backup markers - cryptoStore.resetBackupMarkers() - } - - /** - * Send a chunk of keys to backup. - */ - private suspend fun backupKeys() { - Timber.v("backupKeys") - - // Sanity check, as this method can be called after a delay, the state may have change during the delay - if (!isEnabled() || backupOlmPkEncryption == null || keysBackupVersion == null) { - Timber.v("backupKeys: Invalid configuration") - backupAllGroupSessionsCallback?.onFailure(IllegalStateException("Invalid configuration")) - resetBackupAllGroupSessionsListeners() - return - } - - if (getState() === KeysBackupState.BackingUp) { - // Do nothing if we are already backing up - Timber.v("backupKeys: Invalid state: ${getState()}") - return - } - - // Get a chunk of keys to backup - val olmInboundGroupSessionWrappers = cryptoStore.inboundGroupSessionsToBackup(KEY_BACKUP_SEND_KEYS_MAX_COUNT) - - Timber.v("backupKeys: 1 - ${olmInboundGroupSessionWrappers.size} sessions to back up") - - if (olmInboundGroupSessionWrappers.isEmpty()) { - // Backup is up to date - keysBackupStateManager.state = KeysBackupState.ReadyToBackUp - - backupAllGroupSessionsCallback?.onSuccess(Unit) - resetBackupAllGroupSessionsListeners() - return - } - - keysBackupStateManager.state = KeysBackupState.BackingUp - - withContext(coroutineDispatchers.crypto) { - Timber.v("backupKeys: 2 - Encrypting keys") - - // Gather data to send to the homeserver - // roomId -> sessionId -> MXKeyBackupData - val keysBackupData = KeysBackupData() - - olmInboundGroupSessionWrappers.forEach { olmInboundGroupSessionWrapper -> - val roomId = olmInboundGroupSessionWrapper.roomId ?: return@forEach - val olmInboundGroupSession = olmInboundGroupSessionWrapper.session - - try { - encryptGroupSession(olmInboundGroupSessionWrapper) - ?.let { - keysBackupData.roomIdToRoomKeysBackupData - .getOrPut(roomId) { RoomKeysBackupData() } - .sessionIdToKeyBackupData[olmInboundGroupSession.sessionIdentifier()] = it - } - } catch (e: OlmException) { - Timber.e(e, "OlmException") - } - } - - Timber.v("backupKeys: 4 - Sending request") - - // Make the request - val version = keysBackupVersion?.version ?: return@withContext - - try { - val data = storeSessionDataTask - .execute(StoreSessionsDataTask.Params(version, keysBackupData)) - Timber.v("backupKeys: 5a - Request complete") - - // Mark keys as backed up - cryptoStore.markBackupDoneForInboundGroupSessions(olmInboundGroupSessionWrappers) - // we can release the sessions now - olmInboundGroupSessionWrappers.onEach { it.session.releaseSession() } - - if (olmInboundGroupSessionWrappers.size < KEY_BACKUP_SEND_KEYS_MAX_COUNT) { - Timber.v("backupKeys: All keys have been backed up") - onServerDataRetrieved(data.count, data.hash) - - // Note: Changing state will trigger the call to backupAllGroupSessionsCallback.onSuccess() - keysBackupStateManager.state = KeysBackupState.ReadyToBackUp - } else { - Timber.v("backupKeys: Continue to back up keys") - keysBackupStateManager.state = KeysBackupState.WillBackUp - - backupKeys() - } - } catch (failure: Throwable) { - if (failure is Failure.ServerError) { - Timber.e(failure, "backupKeys: backupKeys failed.") - - when (failure.error.code) { - MatrixError.M_NOT_FOUND, - MatrixError.M_WRONG_ROOM_KEYS_VERSION -> { - // Backup has been deleted on the server, or we are not using the last backup version - keysBackupStateManager.state = KeysBackupState.WrongBackUpVersion - backupAllGroupSessionsCallback?.onFailure(failure) - resetBackupAllGroupSessionsListeners() - resetKeysBackupData() - keysBackupVersion = null - - // Do not stay in KeysBackupState.WrongBackUpVersion but check what is available on the homeserver - checkAndStartKeysBackup() - } - else -> - // Come back to the ready state so that we will retry on the next received key - keysBackupStateManager.state = KeysBackupState.ReadyToBackUp - } - } else { - backupAllGroupSessionsCallback?.onFailure(failure) - resetBackupAllGroupSessionsListeners() - - Timber.e("backupKeys: backupKeys failed.") - - // Retry a bit later - keysBackupStateManager.state = KeysBackupState.ReadyToBackUp - maybeBackupKeys() - } - } - } - } - - @VisibleForTesting - @WorkerThread - suspend fun encryptGroupSession(olmInboundGroupSessionWrapper: MXInboundMegolmSessionWrapper): KeyBackupData? { - olmInboundGroupSessionWrapper.safeSessionId ?: return null - olmInboundGroupSessionWrapper.senderKey ?: return null - // Gather information for each key - val device = cryptoStore.deviceWithIdentityKey(olmInboundGroupSessionWrapper.senderKey) - - // Build the m.megolm_backup.v1.curve25519-aes-sha2 data as defined at - // https://github.com/uhoreg/matrix-doc/blob/e2e_backup/proposals/1219-storing-megolm-keys-serverside.md#mmegolm_backupv1curve25519-aes-sha2-key-format - val sessionData = inboundGroupSessionStore - .getInboundGroupSession(olmInboundGroupSessionWrapper.safeSessionId, olmInboundGroupSessionWrapper.senderKey) - ?.let { - withContext(coroutineDispatchers.computation) { - it.mutex.withLock { it.wrapper.exportKeys() } - } - } - ?: return null - val sessionBackupData = mapOf( - "algorithm" to sessionData.algorithm, - "sender_key" to sessionData.senderKey, - "sender_claimed_keys" to sessionData.senderClaimedKeys, - "forwarding_curve25519_key_chain" to (sessionData.forwardingCurve25519KeyChain.orEmpty()), - "session_key" to sessionData.sessionKey, - "org.matrix.msc3061.shared_history" to sessionData.sharedHistory - ) - - val json = MoshiProvider.providesMoshi() - .adapter(Map::class.java) - .toJson(sessionBackupData) - - val encryptedSessionBackupData = try { - withContext(coroutineDispatchers.computation) { - backupOlmPkEncryption?.encrypt(json) - } - } catch (e: OlmException) { - Timber.e(e, "OlmException") - null - } - ?: return null - - // Build backup data for that key - return KeyBackupData( - firstMessageIndex = try { - olmInboundGroupSessionWrapper.session.firstKnownIndex - } catch (e: OlmException) { - Timber.e(e, "OlmException") - 0L - }, - forwardedCount = olmInboundGroupSessionWrapper.sessionData.forwardingCurve25519KeyChain.orEmpty().size, - isVerified = device?.isVerified == true, - sharedHistory = olmInboundGroupSessionWrapper.getSharedKey(), - sessionData = mapOf( - "ciphertext" to encryptedSessionBackupData.mCipherText, - "mac" to encryptedSessionBackupData.mMac, - "ephemeral" to encryptedSessionBackupData.mEphemeralKey - ) - ) - } - - /** - * Returns boolean shared key flag, if enabled with respect to matrix configuration. - */ - private fun MXInboundMegolmSessionWrapper.getSharedKey(): Boolean { - if (!cryptoStore.isShareKeysOnInviteEnabled()) return false - return sessionData.sharedHistory - } - - @VisibleForTesting - @WorkerThread - fun decryptKeyBackupData(keyBackupData: KeyBackupData, sessionId: String, roomId: String, recoveryKey: IBackupRecoveryKey): MegolmSessionData? { - var sessionBackupData: MegolmSessionData? = null - - val jsonObject = keyBackupData.sessionData - - val ciphertext = jsonObject["ciphertext"]?.toString() - val mac = jsonObject["mac"]?.toString() - val ephemeralKey = jsonObject["ephemeral"]?.toString() - - if (ciphertext != null && mac != null && ephemeralKey != null) { - try { - val decrypted = recoveryKey.decryptV1(ephemeralKey, mac, ciphertext) - val moshi = MoshiProvider.providesMoshi() - val adapter = moshi.adapter(MegolmSessionData::class.java) - - sessionBackupData = adapter.fromJson(decrypted) - } catch (e: OlmException) { - Timber.e(e, "OlmException") - } - - if (sessionBackupData != null) { - sessionBackupData = sessionBackupData.copy( - sessionId = sessionId, - roomId = roomId - ) - } - } - - return sessionBackupData - } - - /* ========================================================================================== - * For test only - * ========================================================================================== */ - - // Direct access for test only - @VisibleForTesting - val store - get() = cryptoStore - - @VisibleForTesting - fun createFakeKeysBackupVersion( - keysBackupCreationInfo: MegolmBackupCreationInfo, - callback: MatrixCallback - ) { - @Suppress("UNCHECKED_CAST") - val createKeysBackupVersionBody = CreateKeysBackupVersionBody( - algorithm = keysBackupCreationInfo.algorithm, - authData = keysBackupCreationInfo.authData.toJsonDict() - ) - - createKeysBackupVersionTask - .configureWith(createKeysBackupVersionBody) { - this.callback = callback - } - .executeBy(taskExecutor) - } - - override suspend fun getKeyBackupRecoveryKeyInfo(): SavedKeyBackupKeyInfo - ? { - return cryptoStore.getKeyBackupRecoveryKeyInfo() - } - - override fun saveBackupRecoveryKey( - recoveryKey: IBackupRecoveryKey?, version: String - ? - ) { - cryptoStore.saveBackupRecoveryKey(recoveryKey?.toBase58(), version) - } - - companion object { - // Maximum delay in ms in {@link maybeBackupKeys} - private const val KEY_BACKUP_WAITING_TIME_TO_SEND_KEY_BACKUP_MILLIS = 10_000L - - // Maximum number of keys to send at a time to the homeserver. - private const val KEY_BACKUP_SEND_KEYS_MAX_COUNT = 100 - } - -/* ========================================================================================== - * DEBUG INFO - * ========================================================================================== */ - - override fun toString() = "KeysBackup for $userId" -} diff --git a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/model/rest/KeyVerificationAccept.kt b/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/model/rest/KeyVerificationAccept.kt deleted file mode 100644 index 85ba1762d3..0000000000 --- a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/model/rest/KeyVerificationAccept.kt +++ /dev/null @@ -1,90 +0,0 @@ -/* - * Copyright 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. - */ -package org.matrix.android.sdk.internal.crypto.model.rest - -import com.squareup.moshi.Json -import com.squareup.moshi.JsonClass -import org.matrix.android.sdk.api.session.crypto.model.SendToDeviceObject -import org.matrix.android.sdk.internal.crypto.verification.VerificationInfoAccept -import org.matrix.android.sdk.internal.crypto.verification.VerificationInfoAcceptFactory - -/** - * Sent by Bob to accept a verification from a previously sent m.key.verification.start message. - */ -@JsonClass(generateAdapter = true) -internal data class KeyVerificationAccept( - /** - * string to identify the transaction. - * This string must be unique for the pair of users performing verification for the duration that the transaction is valid. - * Alice’s device should record this ID and use it in future messages in this transaction. - */ - @Json(name = "transaction_id") - override val transactionId: String? = null, - - /** - * The key agreement protocol that Bob’s device has selected to use, out of the list proposed by Alice’s device. - */ - @Json(name = "key_agreement_protocol") - override val keyAgreementProtocol: String? = null, - - /** - * The hash algorithm that Bob’s device has selected to use, out of the list proposed by Alice’s device. - */ - @Json(name = "hash") - override val hash: String? = null, - - /** - * The message authentication code that Bob’s device has selected to use, out of the list proposed by Alice’s device. - */ - @Json(name = "message_authentication_code") - override val messageAuthenticationCode: String? = null, - - /** - * An array of short authentication string methods that Bob’s client (and Bob) understands. Must be a subset of the list proposed by Alice’s device. - */ - @Json(name = "short_authentication_string") - override val shortAuthenticationStrings: List? = null, - - /** - * The hash (encoded as unpadded base64) of the concatenation of the device’s ephemeral public key (QB, encoded as unpadded base64) - * and the canonical JSON representation of the m.key.verification.start message. - */ - @Json(name = "commitment") - override var commitment: String? = null -) : SendToDeviceObject, VerificationInfoAccept { - - override fun toSendToDeviceObject() = this - - companion object : VerificationInfoAcceptFactory { - override fun create( - tid: String, - keyAgreementProtocol: String, - hash: String, - commitment: String, - messageAuthenticationCode: String, - shortAuthenticationStrings: List - ): VerificationInfoAccept { - return KeyVerificationAccept( - transactionId = tid, - keyAgreementProtocol = keyAgreementProtocol, - hash = hash, - commitment = commitment, - messageAuthenticationCode = messageAuthenticationCode, - shortAuthenticationStrings = shortAuthenticationStrings - ) - } - } -} diff --git a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/model/rest/KeyVerificationCancel.kt b/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/model/rest/KeyVerificationCancel.kt deleted file mode 100644 index 2858ef3eed..0000000000 --- a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/model/rest/KeyVerificationCancel.kt +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Copyright 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. - */ -package org.matrix.android.sdk.internal.crypto.model.rest - -import com.squareup.moshi.Json -import com.squareup.moshi.JsonClass -import org.matrix.android.sdk.api.session.crypto.model.SendToDeviceObject -import org.matrix.android.sdk.api.session.crypto.verification.CancelCode -import org.matrix.android.sdk.internal.crypto.verification.VerificationInfoCancel - -/** - * To device event sent by either party to cancel a key verification. - */ -@JsonClass(generateAdapter = true) -internal data class KeyVerificationCancel( - /** - * the transaction ID of the verification to cancel. - */ - @Json(name = "transaction_id") - override val transactionId: String? = null, - - /** - * machine-readable reason for cancelling, see #CancelCode. - */ - override val code: String? = null, - - /** - * human-readable reason for cancelling. This should only be used if the receiving client does not understand the code given. - */ - override val reason: String? = null -) : SendToDeviceObject, VerificationInfoCancel { - - companion object { - fun create(tid: String, cancelCode: CancelCode): KeyVerificationCancel { - return KeyVerificationCancel( - tid, - cancelCode.value, - cancelCode.humanReadable - ) - } - } - - override fun toSendToDeviceObject() = this -} diff --git a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/model/rest/KeyVerificationDone.kt b/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/model/rest/KeyVerificationDone.kt deleted file mode 100644 index e3907914ac..0000000000 --- a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/model/rest/KeyVerificationDone.kt +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright 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. - */ -package org.matrix.android.sdk.internal.crypto.model.rest - -import com.squareup.moshi.Json -import com.squareup.moshi.JsonClass -import org.matrix.android.sdk.api.session.crypto.model.SendToDeviceObject -import org.matrix.android.sdk.internal.crypto.verification.VerificationInfoDone - -/** - * Requests a key verification with another user's devices. - */ -@JsonClass(generateAdapter = true) -internal data class KeyVerificationDone( - @Json(name = "transaction_id") override val transactionId: String? = null -) : SendToDeviceObject, VerificationInfoDone { - - override fun toSendToDeviceObject() = this -} diff --git a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/model/rest/KeyVerificationKey.kt b/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/model/rest/KeyVerificationKey.kt deleted file mode 100644 index a833148b9d..0000000000 --- a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/model/rest/KeyVerificationKey.kt +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright 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. - */ -package org.matrix.android.sdk.internal.crypto.model.rest - -import com.squareup.moshi.Json -import com.squareup.moshi.JsonClass -import org.matrix.android.sdk.api.session.crypto.model.SendToDeviceObject -import org.matrix.android.sdk.internal.crypto.verification.VerificationInfoKey -import org.matrix.android.sdk.internal.crypto.verification.VerificationInfoKeyFactory - -/** - * Sent by both devices to send their ephemeral Curve25519 public key to the other device. - */ -@JsonClass(generateAdapter = true) -internal data class KeyVerificationKey( - /** - * The ID of the transaction that the message is part of. - */ - @Json(name = "transaction_id") override val transactionId: String? = null, - - /** - * The device’s ephemeral public key, as an unpadded base64 string. - */ - @Json(name = "key") override val key: String? = null - -) : SendToDeviceObject, VerificationInfoKey { - - companion object : VerificationInfoKeyFactory { - override fun create(tid: String, pubKey: String): KeyVerificationKey { - return KeyVerificationKey(tid, pubKey) - } - } - - override fun toSendToDeviceObject() = this -} diff --git a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/model/rest/KeyVerificationMac.kt b/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/model/rest/KeyVerificationMac.kt deleted file mode 100644 index 5335428c0f..0000000000 --- a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/model/rest/KeyVerificationMac.kt +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright 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. - */ -package org.matrix.android.sdk.internal.crypto.model.rest - -import com.squareup.moshi.Json -import com.squareup.moshi.JsonClass -import org.matrix.android.sdk.api.session.crypto.model.SendToDeviceObject -import org.matrix.android.sdk.internal.crypto.verification.VerificationInfoMac -import org.matrix.android.sdk.internal.crypto.verification.VerificationInfoMacFactory - -/** - * Sent by both devices to send the MAC of their device key to the other device. - */ -@JsonClass(generateAdapter = true) -internal data class KeyVerificationMac( - @Json(name = "transaction_id") override val transactionId: String? = null, - @Json(name = "mac") override val mac: Map? = null, - @Json(name = "keys") override val keys: String? = null - -) : SendToDeviceObject, VerificationInfoMac { - - override fun toSendToDeviceObject(): SendToDeviceObject? = this - - companion object : VerificationInfoMacFactory { - override fun create(tid: String, mac: Map, keys: String): VerificationInfoMac { - return KeyVerificationMac(tid, mac, keys) - } - } -} diff --git a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/model/rest/KeyVerificationReady.kt b/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/model/rest/KeyVerificationReady.kt deleted file mode 100644 index 860bbe46a6..0000000000 --- a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/model/rest/KeyVerificationReady.kt +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright 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. - */ -package org.matrix.android.sdk.internal.crypto.model.rest - -import com.squareup.moshi.Json -import com.squareup.moshi.JsonClass -import org.matrix.android.sdk.api.session.crypto.model.SendToDeviceObject -import org.matrix.android.sdk.api.session.events.model.toContent -import org.matrix.android.sdk.internal.crypto.verification.VerificationInfoReady - -/** - * Requests a key verification with another user's devices. - */ -@JsonClass(generateAdapter = true) -internal data class KeyVerificationReady( - @Json(name = "from_device") override val fromDevice: String?, - @Json(name = "methods") override val methods: List?, - @Json(name = "transaction_id") override val transactionId: String? = null -) : SendToDeviceObject, VerificationInfoReady { - - override fun toSendToDeviceObject() = this - - override fun toEventContent() = toContent() -} diff --git a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/model/rest/KeyVerificationRequest.kt b/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/model/rest/KeyVerificationRequest.kt deleted file mode 100644 index 388a1a54ae..0000000000 --- a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/model/rest/KeyVerificationRequest.kt +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright 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. - */ -package org.matrix.android.sdk.internal.crypto.model.rest - -import com.squareup.moshi.Json -import com.squareup.moshi.JsonClass -import org.matrix.android.sdk.api.session.crypto.model.SendToDeviceObject -import org.matrix.android.sdk.api.session.events.model.toContent -import org.matrix.android.sdk.internal.crypto.verification.VerificationInfoRequest - -/** - * Requests a key verification with another user's devices. - */ -@JsonClass(generateAdapter = true) -internal data class KeyVerificationRequest( - @Json(name = "from_device") override val fromDevice: String?, - @Json(name = "methods") override val methods: List, - @Json(name = "timestamp") override val timestamp: Long?, - @Json(name = "transaction_id") override val transactionId: String? = null -) : SendToDeviceObject, VerificationInfoRequest { - - override fun toSendToDeviceObject() = this - - override fun toEventContent() = toContent() -} diff --git a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/model/rest/KeyVerificationStart.kt b/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/model/rest/KeyVerificationStart.kt deleted file mode 100644 index f74bad844d..0000000000 --- a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/model/rest/KeyVerificationStart.kt +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright 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. - */ -package org.matrix.android.sdk.internal.crypto.model.rest - -import com.squareup.moshi.Json -import com.squareup.moshi.JsonClass -import org.matrix.android.sdk.api.session.crypto.model.SendToDeviceObject -import org.matrix.android.sdk.internal.crypto.verification.VerificationInfoStart -import org.matrix.android.sdk.internal.util.JsonCanonicalizer - -/** - * Sent by Alice to initiate an interactive key verification. - */ -@JsonClass(generateAdapter = true) -internal data class KeyVerificationStart( - @Json(name = "from_device") override val fromDevice: String? = null, - @Json(name = "method") override val method: String? = null, - @Json(name = "transaction_id") override val transactionId: String? = null, - @Json(name = "key_agreement_protocols") override val keyAgreementProtocols: List? = null, - @Json(name = "hashes") override val hashes: List? = null, - @Json(name = "message_authentication_codes") override val messageAuthenticationCodes: List? = null, - @Json(name = "short_authentication_string") override val shortAuthenticationStrings: List? = null, - // For QR code verification - @Json(name = "secret") override val sharedSecret: String? = null -) : SendToDeviceObject, VerificationInfoStart { - - override fun toCanonicalJson(): String { - return JsonCanonicalizer.getCanonicalJson(KeyVerificationStart::class.java, this) - } - - override fun toSendToDeviceObject() = this -} diff --git a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/store/IMXCryptoStore.kt b/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/store/IMXCryptoStore.kt deleted file mode 100644 index fc882e5c1d..0000000000 --- a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/store/IMXCryptoStore.kt +++ /dev/null @@ -1,498 +0,0 @@ -/* - * Copyright 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. - */ - -package org.matrix.android.sdk.internal.crypto.store - -import androidx.lifecycle.LiveData -import androidx.paging.PagedList -import org.matrix.android.sdk.api.session.crypto.GlobalCryptoConfig -import org.matrix.android.sdk.api.session.crypto.NewSessionListener -import org.matrix.android.sdk.api.session.crypto.OutgoingKeyRequest -import org.matrix.android.sdk.api.session.crypto.OutgoingRoomKeyRequestState -import org.matrix.android.sdk.api.session.crypto.crosssigning.MXCrossSigningInfo -import org.matrix.android.sdk.api.session.crypto.crosssigning.PrivateKeysInfo -import org.matrix.android.sdk.api.session.crypto.crosssigning.UserIdentity -import org.matrix.android.sdk.api.session.crypto.keysbackup.SavedKeyBackupKeyInfo -import org.matrix.android.sdk.api.session.crypto.model.AuditTrail -import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo -import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap -import org.matrix.android.sdk.api.session.crypto.model.RoomKeyRequestBody -import org.matrix.android.sdk.api.session.crypto.model.TrailType -import org.matrix.android.sdk.api.session.events.model.Event -import org.matrix.android.sdk.api.session.events.model.content.RoomKeyWithHeldContent -import org.matrix.android.sdk.api.session.events.model.content.WithHeldCode -import org.matrix.android.sdk.api.util.Optional -import org.matrix.android.sdk.internal.crypto.model.MXInboundMegolmSessionWrapper -import org.matrix.android.sdk.internal.crypto.model.OlmSessionWrapper -import org.matrix.android.sdk.internal.crypto.model.OutboundGroupSessionWrapper -import org.matrix.android.sdk.internal.crypto.store.db.model.KeysBackupDataEntity -import org.matrix.olm.OlmAccount -import org.matrix.olm.OlmOutboundGroupSession - -/** - * The crypto data store. - */ -internal interface IMXCryptoStore : IMXCommonCryptoStore { - - /** - * @return the device id - */ - fun getDeviceId(): String - - /** - * @return the olm account - */ - fun doWithOlmAccount(block: (OlmAccount) -> T): T - - fun getOrCreateOlmAccount(): OlmAccount - - /** - * Retrieve the known inbound group sessions. - * - * @return the list of all known group sessions, to export them. - */ - fun getInboundGroupSessions(): List - - /** - * Retrieve the known inbound group sessions for the specified room. - * - * @param roomId The roomId that the sessions will be returned - * @return the list of all known group sessions, for the provided roomId - */ - fun getInboundGroupSessions(roomId: String): List - - /** - * Enable or disable key gossiping. - * Default is true. - * If set to false this device won't send key_request nor will accept key forwarded - */ - fun enableKeyGossiping(enable: Boolean) - - fun isKeyGossipingEnabled(): Boolean - - /** - * As per MSC3061. - * If true will make it possible to share part of e2ee room history - * on invite depending on the room visibility setting. - */ - fun enableShareKeyOnInvite(enable: Boolean) - - /** - * As per MSC3061. - * If true will make it possible to share part of e2ee room history - * on invite depending on the room visibility setting. - */ - fun isShareKeysOnInviteEnabled(): Boolean - - /** - * Provides the rooms ids list in which the messages are not encrypted for the unverified devices. - * - * @return the room Ids list - */ - fun getRoomsListBlacklistUnverifiedDevices(): List - - /** - * Get the current keys backup version. - */ - fun getKeyBackupVersion(): String? - - /** - * Set the current keys backup version. - * - * @param keyBackupVersion the keys backup version or null to delete it - */ - fun setKeyBackupVersion(keyBackupVersion: String?) - - /** - * Get the current keys backup local data. - */ - fun getKeysBackupData(): KeysBackupDataEntity? - - /** - * Set the keys backup local data. - * - * @param keysBackupData the keys backup local data, or null to erase data - */ - fun setKeysBackupData(keysBackupData: KeysBackupDataEntity?) - - /** - * @return the devices statuses map (userId -> tracking status) - */ - fun getDeviceTrackingStatuses(): Map - - /** - * Indicate if the store contains data for the passed account. - * - * @return true means that the user enabled the crypto in a previous session - */ - fun hasData(): Boolean - - /** - * Delete the crypto store for the passed credentials. - */ - fun deleteStore() - - /** - * Store the device id. - * - * @param deviceId the device id - */ - fun storeDeviceId(deviceId: String) - - /** - * Store the end to end account for the logged-in user. - */ - fun saveOlmAccount() - - /** - * Retrieve a device for a user. - * - * @param userId the user's id. - * @param deviceId the device id. - * @return the device - */ - fun getUserDevice(userId: String, deviceId: String): CryptoDeviceInfo? - - /** - * Retrieve a device by its identity key. - * - * @param identityKey the device identity key (`MXDeviceInfo.identityKey`) - * @return the device or null if not found - */ - fun deviceWithIdentityKey(identityKey: String): CryptoDeviceInfo? - - /** - * Store the known devices for a user. - * - * @param userId The user's id. - * @param devices A map from device id to 'MXDevice' object for the device. - */ - fun storeUserDevices(userId: String, devices: Map?) - - /** - * Store the cross signing keys for the user userId. - */ - fun storeUserIdentity( - userId: String, - userIdentity: UserIdentity - ) - - /** - * Retrieve the known devices for a user. - * - * @param userId The user's id. - * @return The devices map if some devices are known, else null - */ - fun getUserDevices(userId: String): Map? - - fun getUserDeviceList(userId: String): List? - -// fun getUserDeviceListFlow(userId: String): Flow> - - fun getLiveDeviceList(userId: String): LiveData> - - fun getLiveDeviceList(userIds: List): LiveData> - - // TODO temp - fun getLiveDeviceList(): LiveData> - - fun getLiveDeviceWithId(deviceId: String): LiveData> - - /** - * Store the crypto algorithm for a room. - * - * @param roomId the id of the room. - * @param algorithm the algorithm. - */ - fun storeRoomAlgorithm(roomId: String, algorithm: String?) - - fun shouldShareHistory(roomId: String): Boolean - - /** - * Store a session between the logged-in user and another device. - * - * @param olmSessionWrapper the end-to-end session. - * @param deviceKey the public key of the other device. - */ - fun storeSession(olmSessionWrapper: OlmSessionWrapper, deviceKey: String) - - /** - * Retrieve all end-to-end session ids between our own device and another - * device. - * - * @param deviceKey the public key of the other device. - * @return A set of sessionId, or null if device is not known - */ - fun getDeviceSessionIds(deviceKey: String): List? - - /** - * Retrieve an end-to-end session between our own device and another - * device. - * - * @param sessionId the session Id. - * @param deviceKey the public key of the other device. - * @return The Base64 end-to-end session, or null if not found - */ - fun getDeviceSession(sessionId: String, deviceKey: String): OlmSessionWrapper? - - /** - * Retrieve the last used sessionId, regarding `lastReceivedMessageTs`, or null if no session exist. - * - * @param deviceKey the public key of the other device. - * @return last used sessionId, or null if not found - */ - fun getLastUsedSessionId(deviceKey: String): String? - - /** - * Store inbound group sessions. - * - * @param sessions the inbound group sessions to store. - */ - fun storeInboundGroupSessions(sessions: List) - - /** - * Retrieve an inbound group session, filtering shared history. - * - * @param sessionId the session identifier. - * @param senderKey the base64-encoded curve25519 key of the sender. - * @param sharedHistory filter inbound session with respect to shared history field - * @return an inbound group session. - */ - fun getInboundGroupSession(sessionId: String, senderKey: String, sharedHistory: Boolean): MXInboundMegolmSessionWrapper? - - /** - * Get the current outbound group session for this encrypted room. - */ - fun getCurrentOutboundGroupSessionForRoom(roomId: String): OutboundGroupSessionWrapper? - - /** - * Get the current outbound group session for this encrypted room. - */ - fun storeCurrentOutboundGroupSessionForRoom(roomId: String, outboundGroupSession: OlmOutboundGroupSession?) - - /** - * Remove an inbound group session. - * - * @param sessionId the session identifier. - * @param senderKey the base64-encoded curve25519 key of the sender. - */ - fun removeInboundGroupSession(sessionId: String, senderKey: String) - - /* ========================================================================================== - * Keys backup - * ========================================================================================== */ - - /** - * Mark all inbound group sessions as not backed up. - */ - fun resetBackupMarkers() - - /** - * Mark inbound group sessions as backed up on the user homeserver. - * - * @param olmInboundGroupSessionWrappers the sessions - */ - fun markBackupDoneForInboundGroupSessions(olmInboundGroupSessionWrappers: List) - - /** - * Retrieve inbound group sessions that are not yet backed up. - * - * @param limit the maximum number of sessions to return. - * @return an array of non backed up inbound group sessions. - */ - fun inboundGroupSessionsToBackup(limit: Int): List - - /** - * Number of stored inbound group sessions. - * - * @param onlyBackedUp if true, count only session marked as backed up. - * @return a count. - */ - fun inboundGroupSessionsCount(onlyBackedUp: Boolean): Int - - /** - * Save the device statuses. - * - * @param deviceTrackingStatuses the device tracking statuses - */ - fun saveDeviceTrackingStatuses(deviceTrackingStatuses: Map) - - /** - * Get the tracking status of a specified userId devices. - * - * @param userId the user id - * @param defaultValue the default value - * @return the tracking status - */ - fun getDeviceTrackingStatus(userId: String, defaultValue: Int): Int - - /** - * Look for an existing outgoing room key request, and if none is found, return null. - * - * @param requestBody the request body - * @return an OutgoingRoomKeyRequest instance or null - */ - fun getOutgoingRoomKeyRequest(requestBody: RoomKeyRequestBody): OutgoingKeyRequest? - fun getOutgoingRoomKeyRequest(requestId: String): OutgoingKeyRequest? - fun getOutgoingRoomKeyRequest(roomId: String, sessionId: String, algorithm: String, senderKey: String): List - - /** - * Look for an existing outgoing room key request, and if none is found, add a new one. - * - * @param requestBody the request - * @param recipients list of recipients - * @param fromIndex start index - * @return either the same instance as passed in, or the existing one. - */ - fun getOrAddOutgoingRoomKeyRequest(requestBody: RoomKeyRequestBody, recipients: Map>, fromIndex: Int): OutgoingKeyRequest - fun updateOutgoingRoomKeyRequestState(requestId: String, newState: OutgoingRoomKeyRequestState) - fun updateOutgoingRoomKeyRequiredIndex(requestId: String, newIndex: Int) - fun updateOutgoingRoomKeyReply( - roomId: String, - sessionId: String, - algorithm: String, - senderKey: String, - fromDevice: String?, - event: Event - ) - - fun deleteOutgoingRoomKeyRequest(requestId: String) - fun deleteOutgoingRoomKeyRequestInState(state: OutgoingRoomKeyRequestState) - - fun saveIncomingKeyRequestAuditTrail( - requestId: String, - roomId: String, - sessionId: String, - senderKey: String, - algorithm: String, - fromUser: String, - fromDevice: String - ) - - fun saveWithheldAuditTrail( - roomId: String, - sessionId: String, - senderKey: String, - algorithm: String, - code: WithHeldCode, - userId: String, - deviceId: String - ) - - fun saveForwardKeyAuditTrail( - roomId: String, - sessionId: String, - senderKey: String, - algorithm: String, - userId: String, - deviceId: String, - chainIndex: Long? - ) - - fun saveIncomingForwardKeyAuditTrail( - roomId: String, - sessionId: String, - senderKey: String, - algorithm: String, - userId: String, - deviceId: String, - chainIndex: Long? - ) - - fun addNewSessionListener(listener: NewSessionListener) - - fun removeSessionListener(listener: NewSessionListener) - - // ============================================= - // CROSS SIGNING - // ============================================= - - /** - * Gets the current crosssigning info. - */ - fun getMyCrossSigningInfo(): MXCrossSigningInfo? - - fun setMyCrossSigningInfo(info: MXCrossSigningInfo?) - - fun getCrossSigningInfo(userId: String): MXCrossSigningInfo? - fun getLiveCrossSigningInfo(userId: String): LiveData> - -// fun getCrossSigningInfoFlow(userId: String): Flow> - fun setCrossSigningInfo(userId: String, info: MXCrossSigningInfo?) - - fun markMyMasterKeyAsLocallyTrusted(trusted: Boolean) - - fun storePrivateKeysInfo(msk: String?, usk: String?, ssk: String?) - fun storeMSKPrivateKey(msk: String?) - fun storeSSKPrivateKey(ssk: String?) - fun storeUSKPrivateKey(usk: String?) - - fun getCrossSigningPrivateKeys(): PrivateKeysInfo? - fun getLiveCrossSigningPrivateKeys(): LiveData> -// fun getCrossSigningPrivateKeysFlow(): Flow> - - fun getGlobalCryptoConfig(): GlobalCryptoConfig - - fun saveBackupRecoveryKey(recoveryKey: String?, version: String?) - fun getKeyBackupRecoveryKeyInfo(): SavedKeyBackupKeyInfo? - - fun setUserKeysAsTrusted(userId: String, trusted: Boolean = true) - fun setDeviceTrust(userId: String, deviceId: String, crossSignedVerified: Boolean, locallyVerified: Boolean?) - - fun clearOtherUserTrust() - - fun updateUsersTrust(check: (String) -> Boolean) - - fun addWithHeldMegolmSession(withHeldContent: RoomKeyWithHeldContent) - fun getWithHeldMegolmSession(roomId: String, sessionId: String): RoomKeyWithHeldContent? - - fun markedSessionAsShared( - roomId: String?, - sessionId: String, - userId: String, - deviceId: String, - deviceIdentityKey: String, - chainIndex: Int - ) - - /** - * Query for information on this session sharing history. - * @return SharedSessionResult - * if found is true then this session was initialy shared with that user|device, - * in this case chainIndex is not nullindicates the ratchet position. - * In found is false, chainIndex is null - */ - fun getSharedSessionInfo(roomId: String?, sessionId: String, deviceInfo: CryptoDeviceInfo): SharedSessionResult - data class SharedSessionResult(val found: Boolean, val chainIndex: Int?) - - fun getSharedWithInfo(roomId: String?, sessionId: String): MXUsersDevicesMap - // Dev tools - - fun getOutgoingRoomKeyRequests(): List - fun getOutgoingRoomKeyRequestsPaged(): LiveData> - fun getGossipingEventsTrail(): LiveData> - fun getGossipingEventsTrail(type: TrailType, mapper: ((AuditTrail) -> T)): LiveData> - fun getGossipingEvents(): List - - fun setDeviceKeysUploaded(uploaded: Boolean) - fun areDeviceKeysUploaded(): Boolean - fun getOutgoingRoomKeyRequests(inStates: Set): List - - /** - * Store a bunch of data related to the users. @See [UserDataToStore]. - */ - fun storeData(userDataToStore: UserDataToStore) -} diff --git a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStore.kt b/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStore.kt deleted file mode 100644 index e3595f6618..0000000000 --- a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStore.kt +++ /dev/null @@ -1,1891 +0,0 @@ -/* - * Copyright 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. - */ - -package org.matrix.android.sdk.internal.crypto.store.db - -import androidx.lifecycle.LiveData -import androidx.lifecycle.Transformations -import androidx.paging.LivePagedListBuilder -import androidx.paging.PagedList -import com.zhuinden.monarchy.Monarchy -import io.realm.Realm -import io.realm.RealmConfiguration -import io.realm.Sort -import io.realm.kotlin.createObject -import io.realm.kotlin.where -import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_MEGOLM -import org.matrix.android.sdk.api.extensions.tryOrNull -import org.matrix.android.sdk.api.logger.LoggerTag -import org.matrix.android.sdk.api.session.crypto.GlobalCryptoConfig -import org.matrix.android.sdk.api.session.crypto.NewSessionListener -import org.matrix.android.sdk.api.session.crypto.OutgoingKeyRequest -import org.matrix.android.sdk.api.session.crypto.OutgoingRoomKeyRequestState -import org.matrix.android.sdk.api.session.crypto.crosssigning.MXCrossSigningInfo -import org.matrix.android.sdk.api.session.crypto.crosssigning.PrivateKeysInfo -import org.matrix.android.sdk.api.session.crypto.crosssigning.UserIdentity -import org.matrix.android.sdk.api.session.crypto.keysbackup.BackupUtils -import org.matrix.android.sdk.api.session.crypto.keysbackup.SavedKeyBackupKeyInfo -import org.matrix.android.sdk.api.session.crypto.model.AuditTrail -import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo -import org.matrix.android.sdk.api.session.crypto.model.CryptoRoomInfo -import org.matrix.android.sdk.api.session.crypto.model.DeviceInfo -import org.matrix.android.sdk.api.session.crypto.model.ForwardInfo -import org.matrix.android.sdk.api.session.crypto.model.IncomingKeyRequestInfo -import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap -import org.matrix.android.sdk.api.session.crypto.model.RoomKeyRequestBody -import org.matrix.android.sdk.api.session.crypto.model.TrailType -import org.matrix.android.sdk.api.session.crypto.model.WithheldInfo -import org.matrix.android.sdk.api.session.events.model.Event -import org.matrix.android.sdk.api.session.events.model.content.EncryptionEventContent -import org.matrix.android.sdk.api.session.events.model.content.RoomKeyWithHeldContent -import org.matrix.android.sdk.api.session.events.model.content.WithHeldCode -import org.matrix.android.sdk.api.util.Optional -import org.matrix.android.sdk.api.util.toOptional -import org.matrix.android.sdk.internal.crypto.model.MXInboundMegolmSessionWrapper -import org.matrix.android.sdk.internal.crypto.model.OlmSessionWrapper -import org.matrix.android.sdk.internal.crypto.model.OutboundGroupSessionWrapper -import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore -import org.matrix.android.sdk.internal.crypto.store.UserDataToStore -import org.matrix.android.sdk.internal.crypto.store.db.mapper.CrossSigningKeysMapper -import org.matrix.android.sdk.internal.crypto.store.db.mapper.CryptoRoomInfoMapper -import org.matrix.android.sdk.internal.crypto.store.db.mapper.MyDeviceLastSeenInfoEntityMapper -import org.matrix.android.sdk.internal.crypto.store.db.model.AuditTrailEntity -import org.matrix.android.sdk.internal.crypto.store.db.model.AuditTrailEntityFields -import org.matrix.android.sdk.internal.crypto.store.db.model.AuditTrailMapper -import org.matrix.android.sdk.internal.crypto.store.db.model.CrossSigningInfoEntity -import org.matrix.android.sdk.internal.crypto.store.db.model.CrossSigningInfoEntityFields -import org.matrix.android.sdk.internal.crypto.store.db.model.CryptoMapper -import org.matrix.android.sdk.internal.crypto.store.db.model.CryptoMetadataEntity -import org.matrix.android.sdk.internal.crypto.store.db.model.CryptoRoomEntity -import org.matrix.android.sdk.internal.crypto.store.db.model.CryptoRoomEntityFields -import org.matrix.android.sdk.internal.crypto.store.db.model.DeviceInfoEntity -import org.matrix.android.sdk.internal.crypto.store.db.model.DeviceInfoEntityFields -import org.matrix.android.sdk.internal.crypto.store.db.model.KeysBackupDataEntity -import org.matrix.android.sdk.internal.crypto.store.db.model.MyDeviceLastSeenInfoEntity -import org.matrix.android.sdk.internal.crypto.store.db.model.MyDeviceLastSeenInfoEntityFields -import org.matrix.android.sdk.internal.crypto.store.db.model.OlmInboundGroupSessionEntity -import org.matrix.android.sdk.internal.crypto.store.db.model.OlmInboundGroupSessionEntityFields -import org.matrix.android.sdk.internal.crypto.store.db.model.OlmSessionEntity -import org.matrix.android.sdk.internal.crypto.store.db.model.OlmSessionEntityFields -import org.matrix.android.sdk.internal.crypto.store.db.model.OutboundGroupSessionInfoEntity -import org.matrix.android.sdk.internal.crypto.store.db.model.OutgoingKeyRequestEntity -import org.matrix.android.sdk.internal.crypto.store.db.model.OutgoingKeyRequestEntityFields -import org.matrix.android.sdk.internal.crypto.store.db.model.SharedSessionEntity -import org.matrix.android.sdk.internal.crypto.store.db.model.TrustLevelEntity -import org.matrix.android.sdk.internal.crypto.store.db.model.UserEntity -import org.matrix.android.sdk.internal.crypto.store.db.model.UserEntityFields -import org.matrix.android.sdk.internal.crypto.store.db.model.WithHeldSessionEntity -import org.matrix.android.sdk.internal.crypto.store.db.model.createPrimaryKey -import org.matrix.android.sdk.internal.crypto.store.db.model.deleteOnCascade -import org.matrix.android.sdk.internal.crypto.store.db.query.create -import org.matrix.android.sdk.internal.crypto.store.db.query.delete -import org.matrix.android.sdk.internal.crypto.store.db.query.get -import org.matrix.android.sdk.internal.crypto.store.db.query.getById -import org.matrix.android.sdk.internal.crypto.store.db.query.getOrCreate -import org.matrix.android.sdk.internal.crypto.util.RequestIdHelper -import org.matrix.android.sdk.internal.di.CryptoDatabase -import org.matrix.android.sdk.internal.di.DeviceId -import org.matrix.android.sdk.internal.di.MoshiProvider -import org.matrix.android.sdk.internal.di.UserId -import org.matrix.android.sdk.internal.extensions.clearWith -import org.matrix.android.sdk.internal.session.SessionScope -import org.matrix.android.sdk.internal.util.time.Clock -import org.matrix.olm.OlmAccount -import org.matrix.olm.OlmException -import org.matrix.olm.OlmOutboundGroupSession -import timber.log.Timber -import java.util.concurrent.Executors -import java.util.concurrent.TimeUnit -import javax.inject.Inject - -private val loggerTag = LoggerTag("RealmCryptoStore", LoggerTag.CRYPTO) - -@SessionScope -internal class RealmCryptoStore @Inject constructor( - @CryptoDatabase private val realmConfiguration: RealmConfiguration, - private val crossSigningKeysMapper: CrossSigningKeysMapper, - @UserId private val userId: String, - @DeviceId private val deviceId: String, - private val clock: Clock, - private val myDeviceLastSeenInfoEntityMapper: MyDeviceLastSeenInfoEntityMapper, -) : IMXCryptoStore { - - /* ========================================================================================== - * Memory cache, to correctly release JNI objects - * ========================================================================================== */ - - // The olm account - private var olmAccount: OlmAccount? = null - - private val newSessionListeners = ArrayList() - - override fun addNewSessionListener(listener: NewSessionListener) { - if (!newSessionListeners.contains(listener)) newSessionListeners.add(listener) - } - - override fun removeSessionListener(listener: NewSessionListener) { - newSessionListeners.remove(listener) - } - - private val monarchyWriteAsyncExecutor = Executors.newSingleThreadExecutor() - - private val monarchy = Monarchy.Builder() - .setRealmConfiguration(realmConfiguration) - .setWriteAsyncExecutor(monarchyWriteAsyncExecutor) - .build() - - init { - // Ensure CryptoMetadataEntity is inserted in DB - doRealmTransaction("init", realmConfiguration) { realm -> - var currentMetadata = realm.where().findFirst() - - var deleteAll = false - - if (currentMetadata != null) { - // Check credentials - // The device id may not have been provided in credentials. - // Check it only if provided, else trust the stored one. - if (currentMetadata.userId != userId || deviceId != currentMetadata.deviceId) { - Timber.w("## open() : Credentials do not match, close this store and delete data") - deleteAll = true - currentMetadata = null - } - } - - if (currentMetadata == null) { - if (deleteAll) { - realm.deleteAll() - } - - // Metadata not found, or database cleaned, create it - realm.createObject(CryptoMetadataEntity::class.java, userId).apply { - deviceId = this@RealmCryptoStore.deviceId - } - } - } - } - /* ========================================================================================== - * Other data - * ========================================================================================== */ - - override fun hasData(): Boolean { - return doWithRealm(realmConfiguration) { - !it.isEmpty && - // Check if there is a MetaData object - it.where().count() > 0 - } - } - - override fun deleteStore() { - doRealmTransaction("deleteStore", realmConfiguration) { - it.deleteAll() - } - } - - override fun open() { - } - - override fun close() { - // Ensure no async request will be run later - val tasks = monarchyWriteAsyncExecutor.shutdownNow() - Timber.w("Closing RealmCryptoStore, ${tasks.size} async task(s) cancelled") - tryOrNull("Interrupted") { - // Wait 1 minute max - monarchyWriteAsyncExecutor.awaitTermination(1, TimeUnit.MINUTES) - } - - olmAccount?.releaseAccount() - } - - override fun storeDeviceId(deviceId: String) { - doRealmTransaction("storeDeviceId", realmConfiguration) { - it.where().findFirst()?.deviceId = deviceId - } - } - - override fun getDeviceId(): String { - return doWithRealm(realmConfiguration) { - it.where().findFirst()?.deviceId - } ?: "" - } - - override fun saveOlmAccount() { - doRealmTransaction("saveOlmAccount", realmConfiguration) { - it.where().findFirst()?.putOlmAccount(olmAccount) - } - } - - /** - * Olm account access should be synchronized. - */ - override fun doWithOlmAccount(block: (OlmAccount) -> T): T { - return olmAccount!!.let { olmAccount -> - synchronized(olmAccount) { - block.invoke(olmAccount) - } - } - } - - @Synchronized - override fun getOrCreateOlmAccount(): OlmAccount { - doRealmTransaction("getOrCreateOlmAccount", realmConfiguration) { - val metaData = it.where().findFirst() - val existing = metaData!!.getOlmAccount() - if (existing == null) { - Timber.d("## Crypto Creating olm account") - val created = OlmAccount() - metaData.putOlmAccount(created) - olmAccount = created - } else { - Timber.d("## Crypto Access existing account") - olmAccount = existing - } - } - return olmAccount!! - } - - override fun getUserDevice(userId: String, deviceId: String): CryptoDeviceInfo? { - return doWithRealm(realmConfiguration) { - it.where() - .equalTo(DeviceInfoEntityFields.PRIMARY_KEY, DeviceInfoEntity.createPrimaryKey(userId, deviceId)) - .findFirst() - ?.let { deviceInfo -> - CryptoMapper.mapToModel(deviceInfo) - } - } - } - - override fun deviceWithIdentityKey(identityKey: String): CryptoDeviceInfo? { - return doWithRealm(realmConfiguration) { realm -> - realm.where() - .contains(DeviceInfoEntityFields.KEYS_MAP_JSON, identityKey) - .findAll() - .mapNotNull { CryptoMapper.mapToModel(it) } - .firstOrNull { - it.identityKey() == identityKey - } - } - } - - override fun deviceWithIdentityKey(userId: String, identityKey: String): CryptoDeviceInfo? { - return doWithRealm(realmConfiguration) { realm -> - realm.where() - .equalTo(DeviceInfoEntityFields.USER_ID, userId) - .contains(DeviceInfoEntityFields.KEYS_MAP_JSON, identityKey) - .findAll() - .mapNotNull { CryptoMapper.mapToModel(it) } - .firstOrNull { - it.identityKey() == identityKey - } - } - } - - override fun storeUserDevices(userId: String, devices: Map?) { - doRealmTransaction("storeUserDevices", realmConfiguration) { realm -> - storeUserDevices(realm, userId, devices) - } - } - - private fun storeUserDevices(realm: Realm, userId: String, devices: Map?) { - if (devices == null) { - Timber.d("Remove user $userId") - // Remove the user - UserEntity.delete(realm, userId) - } else { - val userEntity = UserEntity.getOrCreate(realm, userId) - // First delete the removed devices - val deviceIds = devices.keys - userEntity.devices.toTypedArray().iterator().let { - while (it.hasNext()) { - val deviceInfoEntity = it.next() - if (deviceInfoEntity.deviceId !in deviceIds) { - Timber.d("Remove device ${deviceInfoEntity.deviceId} of user $userId") - deviceInfoEntity.deleteOnCascade() - } - } - } - // Then update existing devices or add new one - devices.values.forEach { cryptoDeviceInfo -> - val existingDeviceInfoEntity = userEntity.devices.firstOrNull { it.deviceId == cryptoDeviceInfo.deviceId } - if (existingDeviceInfoEntity == null) { - // Add the device - Timber.d("Add device ${cryptoDeviceInfo.deviceId} of user $userId") - val newEntity = CryptoMapper.mapToEntity(cryptoDeviceInfo) - newEntity.firstTimeSeenLocalTs = clock.epochMillis() - userEntity.devices.add(newEntity) - } else { - // Update the device - Timber.d("Update device ${cryptoDeviceInfo.deviceId} of user $userId") - CryptoMapper.updateDeviceInfoEntity(existingDeviceInfoEntity, cryptoDeviceInfo) - } - } - } - } - - override fun storeUserIdentity( - userId: String, - userIdentity: UserIdentity, - ) { - doRealmTransaction("storeUserIdentity", realmConfiguration) { realm -> - storeUserIdentity(realm, userId, userIdentity) - } - } - - private fun storeUserIdentity( - realm: Realm, - userId: String, - userIdentity: UserIdentity, - ) { - UserEntity.getOrCreate(realm, userId) - .let { userEntity -> - if (userIdentity.masterKey == null || userIdentity.selfSigningKey == null) { - // The user has disabled cross signing? - userEntity.crossSigningInfoEntity?.deleteOnCascade() - userEntity.crossSigningInfoEntity = null - } else { - var shouldResetMyDevicesLocalTrust = false - CrossSigningInfoEntity.getOrCreate(realm, userId).let { signingInfo -> - // What should we do if we detect a change of the keys? - val existingMaster = signingInfo.getMasterKey() - if (existingMaster != null && existingMaster.publicKeyBase64 == userIdentity.masterKey.unpaddedBase64PublicKey) { - crossSigningKeysMapper.update(existingMaster, userIdentity.masterKey) - } else { - Timber.d("## CrossSigning MSK change for $userId") - val keyEntity = crossSigningKeysMapper.map(userIdentity.masterKey) - signingInfo.setMasterKey(keyEntity) - if (userId == this.userId) { - shouldResetMyDevicesLocalTrust = true - // my msk has changed! clear my private key - // Could we have some race here? e.g I am the one that did change the keys - // could i get this update to early and clear the private keys? - // -> initializeCrossSigning is guarding for that by storing all at once - realm.where().findFirst()?.apply { - xSignMasterPrivateKey = null - } - } - } - - val existingSelfSigned = signingInfo.getSelfSignedKey() - if (existingSelfSigned != null && existingSelfSigned.publicKeyBase64 == userIdentity.selfSigningKey.unpaddedBase64PublicKey) { - crossSigningKeysMapper.update(existingSelfSigned, userIdentity.selfSigningKey) - } else { - Timber.d("## CrossSigning SSK change for $userId") - val keyEntity = crossSigningKeysMapper.map(userIdentity.selfSigningKey) - signingInfo.setSelfSignedKey(keyEntity) - if (userId == this.userId) { - shouldResetMyDevicesLocalTrust = true - // my ssk has changed! clear my private key - realm.where().findFirst()?.apply { - xSignSelfSignedPrivateKey = null - } - } - } - - // Only for me - if (userIdentity.userSigningKey != null) { - val existingUSK = signingInfo.getUserSigningKey() - if (existingUSK != null && existingUSK.publicKeyBase64 == userIdentity.userSigningKey.unpaddedBase64PublicKey) { - crossSigningKeysMapper.update(existingUSK, userIdentity.userSigningKey) - } else { - Timber.d("## CrossSigning USK change for $userId") - val keyEntity = crossSigningKeysMapper.map(userIdentity.userSigningKey) - signingInfo.setUserSignedKey(keyEntity) - if (userId == this.userId) { - shouldResetMyDevicesLocalTrust = true - // my usk has changed! clear my private key - realm.where().findFirst()?.apply { - xSignUserPrivateKey = null - } - } - } - } - - // When my cross signing keys are reset, we consider clearing all existing device trust - if (shouldResetMyDevicesLocalTrust) { - realm.where() - .equalTo(UserEntityFields.USER_ID, this.userId) - .findFirst() - ?.devices?.forEach { - it?.trustLevelEntity?.crossSignedVerified = false - it?.trustLevelEntity?.locallyVerified = it.deviceId == deviceId - } - } - userEntity.crossSigningInfoEntity = signingInfo - } - } - } - } - - override fun getCrossSigningPrivateKeys(): PrivateKeysInfo? { - return doWithRealm(realmConfiguration) { realm -> - realm.where() - .findFirst() - ?.let { - PrivateKeysInfo( - master = it.xSignMasterPrivateKey, - selfSigned = it.xSignSelfSignedPrivateKey, - user = it.xSignUserPrivateKey - ) - } - } - } - - override fun getLiveCrossSigningPrivateKeys(): LiveData> { - val liveData = monarchy.findAllMappedWithChanges( - { realm: Realm -> - realm - .where() - }, - { - PrivateKeysInfo( - master = it.xSignMasterPrivateKey, - selfSigned = it.xSignSelfSignedPrivateKey, - user = it.xSignUserPrivateKey - ) - } - ) - return Transformations.map(liveData) { - it.firstOrNull().toOptional() - } - } - - override fun getLiveCrossSigningInfo(userId: String): LiveData> { - val liveData = monarchy.findAllMappedWithChanges( - { realm: Realm -> - realm.where(CrossSigningInfoEntity::class.java) - .equalTo(CrossSigningInfoEntityFields.USER_ID, userId) - }, - { - mapCrossSigningInfoEntity(it) - } - ) - return Transformations.map(liveData) { - it.firstOrNull().toOptional() - } - } - - override fun getGlobalCryptoConfig(): GlobalCryptoConfig { - return doWithRealm(realmConfiguration) { realm -> - realm.where().findFirst() - ?.let { - GlobalCryptoConfig( - globalBlockUnverifiedDevices = it.globalBlacklistUnverifiedDevices, - globalEnableKeyGossiping = it.globalEnableKeyGossiping, - enableKeyForwardingOnInvite = it.enableKeyForwardingOnInvite - ) - } ?: GlobalCryptoConfig(false, false, false) - } - } - - override fun getLiveGlobalCryptoConfig(): LiveData { - val liveData = monarchy.findAllMappedWithChanges( - { realm: Realm -> - realm - .where() - }, - { - GlobalCryptoConfig( - globalBlockUnverifiedDevices = it.globalBlacklistUnverifiedDevices, - globalEnableKeyGossiping = it.globalEnableKeyGossiping, - enableKeyForwardingOnInvite = it.enableKeyForwardingOnInvite - ) - } - ) - return Transformations.map(liveData) { - it.firstOrNull() ?: GlobalCryptoConfig(false, false, false) - } - } - - override fun storePrivateKeysInfo(msk: String?, usk: String?, ssk: String?) { - Timber.v("## CRYPTO | *** storePrivateKeysInfo ${msk != null}, ${usk != null}, ${ssk != null}") - doRealmTransaction("storePrivateKeysInfo", realmConfiguration) { realm -> - realm.where().findFirst()?.apply { - xSignMasterPrivateKey = msk - xSignUserPrivateKey = usk - xSignSelfSignedPrivateKey = ssk - } - } - } - - override fun saveBackupRecoveryKey(recoveryKey: String?, version: String?) { - doRealmTransaction("saveBackupRecoveryKey", realmConfiguration) { realm -> - realm.where().findFirst()?.apply { - keyBackupRecoveryKey = recoveryKey - keyBackupRecoveryKeyVersion = version - } - } - } - - override fun getKeyBackupRecoveryKeyInfo(): SavedKeyBackupKeyInfo? { - return doWithRealm(realmConfiguration) { realm -> - realm.where() - .findFirst() - ?.let { - val key = it.keyBackupRecoveryKey - val version = it.keyBackupRecoveryKeyVersion - if (!key.isNullOrBlank() && !version.isNullOrBlank()) { - BackupUtils.recoveryKeyFromBase58(key)?.let { recoveryKey -> - SavedKeyBackupKeyInfo(recoveryKey = recoveryKey, version = version) - } - } else { - null - } - } - } - } - - override fun storeMSKPrivateKey(msk: String?) { - Timber.v("## CRYPTO | *** storeMSKPrivateKey ${msk != null} ") - doRealmTransaction("storeMSKPrivateKey", realmConfiguration) { realm -> - realm.where().findFirst()?.apply { - xSignMasterPrivateKey = msk - } - } - } - - override fun storeSSKPrivateKey(ssk: String?) { - Timber.v("## CRYPTO | *** storeSSKPrivateKey ${ssk != null} ") - doRealmTransaction("storeSSKPrivateKey", realmConfiguration) { realm -> - realm.where().findFirst()?.apply { - xSignSelfSignedPrivateKey = ssk - } - } - } - - override fun storeUSKPrivateKey(usk: String?) { - Timber.v("## CRYPTO | *** storeUSKPrivateKey ${usk != null} ") - doRealmTransaction("storeUSKPrivateKey", realmConfiguration) { realm -> - realm.where().findFirst()?.apply { - xSignUserPrivateKey = usk - } - } - } - - override fun getUserDevices(userId: String): Map? { - return doWithRealm(realmConfiguration) { - it.where() - .equalTo(UserEntityFields.USER_ID, userId) - .findFirst() - ?.devices - ?.map { deviceInfo -> - CryptoMapper.mapToModel(deviceInfo) - } - ?.associateBy { cryptoDevice -> - cryptoDevice.deviceId - } - } - } - - override fun getUserDeviceList(userId: String): List? { - return doWithRealm(realmConfiguration) { - it.where() - .equalTo(UserEntityFields.USER_ID, userId) - .findFirst() - ?.devices - ?.map { deviceInfo -> - CryptoMapper.mapToModel(deviceInfo) - } - } - } - - override fun getLiveDeviceList(userId: String): LiveData> { - val liveData = monarchy.findAllMappedWithChanges( - { realm: Realm -> - realm - .where() - .equalTo(UserEntityFields.USER_ID, userId) - }, - { entity -> - entity.devices.map { CryptoMapper.mapToModel(it) } - } - ) - return Transformations.map(liveData) { - it.firstOrNull().orEmpty() - } - } - - override fun getLiveDeviceList(userIds: List): LiveData> { - val liveData = monarchy.findAllMappedWithChanges( - { realm: Realm -> - realm - .where() - .`in`(UserEntityFields.USER_ID, userIds.distinct().toTypedArray()) - }, - { entity -> - entity.devices.map { CryptoMapper.mapToModel(it) } - } - ) - return Transformations.map(liveData) { - it.flatten() - } - } - - override fun getLiveDeviceList(): LiveData> { - val liveData = monarchy.findAllMappedWithChanges( - { realm: Realm -> - realm.where() - }, - { entity -> - entity.devices.map { CryptoMapper.mapToModel(it) } - } - ) - return Transformations.map(liveData) { - it.firstOrNull().orEmpty() - } - } - - override fun getLiveDeviceWithId(deviceId: String): LiveData> { - return Transformations.map(getLiveDeviceList()) { devices -> - devices.firstOrNull { it.deviceId == deviceId }.toOptional() - } - } - - override fun getMyDevicesInfo(): List { - return monarchy.fetchAllCopiedSync { - it.where() - }.map { - DeviceInfo( - deviceId = it.deviceId, - lastSeenIp = it.lastSeenIp, - lastSeenTs = it.lastSeenTs, - displayName = it.displayName - ) - } - } - - override fun getLiveMyDevicesInfo(): LiveData> { - return monarchy.findAllMappedWithChanges( - { realm: Realm -> - realm.where() - }, - { entity -> myDeviceLastSeenInfoEntityMapper.map(entity) } - ) - } - - override fun getLiveMyDevicesInfo(deviceId: String): LiveData> { - val liveData = monarchy.findAllMappedWithChanges( - { realm: Realm -> - realm.where() - .equalTo(MyDeviceLastSeenInfoEntityFields.DEVICE_ID, deviceId) - }, - { entity -> myDeviceLastSeenInfoEntityMapper.map(entity) } - ) - - return Transformations.map(liveData) { - it.firstOrNull().toOptional() - } - } - - override fun saveMyDevicesInfo(info: List) { - val entities = info.map { myDeviceLastSeenInfoEntityMapper.map(it) } - doRealmTransactionAsync(realmConfiguration) { realm -> - realm.where().findAll().deleteAllFromRealm() - entities.forEach { - realm.insertOrUpdate(it) - } - } - } - - override fun storeRoomAlgorithm(roomId: String, algorithm: String?) { - doRealmTransaction("storeRoomAlgorithm", realmConfiguration) { - CryptoRoomEntity.getOrCreate(it, roomId).let { entity -> - entity.algorithm = algorithm - // store anyway the new algorithm, but mark the room - // as having been encrypted once whatever, this can never - // go back to false - if (algorithm == MXCRYPTO_ALGORITHM_MEGOLM) { - entity.wasEncryptedOnce = true - } - } - } - } - - override fun getRoomAlgorithm(roomId: String): String? { - return doWithRealm(realmConfiguration) { - CryptoRoomEntity.getById(it, roomId)?.algorithm - } - } - - override fun getRoomCryptoInfo(roomId: String): CryptoRoomInfo? { - return doWithRealm(realmConfiguration) { realm -> - CryptoRoomEntity.getById(realm, roomId)?.let { - CryptoRoomInfoMapper.map(it) - } - } - } - - override fun setAlgorithmInfo(roomId: String, encryption: EncryptionEventContent?) { - doRealmTransaction("setAlgorithmInfo", realmConfiguration) { - CryptoRoomEntity.getOrCreate(it, roomId).let { entity -> - entity.algorithm = encryption?.algorithm - // store anyway the new algorithm, but mark the room - // as having been encrypted once whatever, this can never - // go back to false - if (encryption?.algorithm == MXCRYPTO_ALGORITHM_MEGOLM) { - entity.wasEncryptedOnce = true - entity.rotationPeriodMs = encryption.rotationPeriodMs - entity.rotationPeriodMsgs = encryption.rotationPeriodMsgs - } - } - } - } - - override fun roomWasOnceEncrypted(roomId: String): Boolean { - return doWithRealm(realmConfiguration) { - CryptoRoomEntity.getById(it, roomId)?.wasEncryptedOnce ?: false - } - } - - override fun shouldEncryptForInvitedMembers(roomId: String): Boolean { - return doWithRealm(realmConfiguration) { - CryptoRoomEntity.getById(it, roomId)?.shouldEncryptForInvitedMembers - } - ?: false - } - - override fun shouldShareHistory(roomId: String): Boolean { - if (!isShareKeysOnInviteEnabled()) return false - return doWithRealm(realmConfiguration) { - CryptoRoomEntity.getById(it, roomId)?.shouldShareHistory - } - ?: false - } - - override fun setShouldEncryptForInvitedMembers(roomId: String, shouldEncryptForInvitedMembers: Boolean) { - doRealmTransaction("setShouldEncryptForInvitedMembers", realmConfiguration) { - CryptoRoomEntity.getOrCreate(it, roomId).shouldEncryptForInvitedMembers = shouldEncryptForInvitedMembers - } - } - - override fun setShouldShareHistory(roomId: String, shouldShareHistory: Boolean) { - Timber.tag(loggerTag.value) - .v("setShouldShareHistory for room $roomId is $shouldShareHistory") - doRealmTransaction("setShouldShareHistory", realmConfiguration) { - CryptoRoomEntity.getOrCreate(it, roomId).shouldShareHistory = shouldShareHistory - } - } - - override fun storeSession(olmSessionWrapper: OlmSessionWrapper, deviceKey: String) { - var sessionIdentifier: String? = null - - try { - sessionIdentifier = olmSessionWrapper.olmSession.sessionIdentifier() - } catch (e: OlmException) { - Timber.e(e, "## storeSession() : sessionIdentifier failed") - } - - if (sessionIdentifier != null) { - val key = OlmSessionEntity.createPrimaryKey(sessionIdentifier, deviceKey) - - doRealmTransaction("storeSession", realmConfiguration) { - val realmOlmSession = OlmSessionEntity().apply { - primaryKey = key - sessionId = sessionIdentifier - this.deviceKey = deviceKey - putOlmSession(olmSessionWrapper.olmSession) - lastReceivedMessageTs = olmSessionWrapper.lastReceivedMessageTs - } - - it.insertOrUpdate(realmOlmSession) - } - } - } - - override fun getDeviceSession(sessionId: String, deviceKey: String): OlmSessionWrapper? { - val key = OlmSessionEntity.createPrimaryKey(sessionId, deviceKey) - return doRealmQueryAndCopy(realmConfiguration) { - it.where() - .equalTo(OlmSessionEntityFields.PRIMARY_KEY, key) - .findFirst() - } - ?.let { - val olmSession = it.getOlmSession() - if (olmSession != null && it.sessionId != null) { - return@let OlmSessionWrapper(olmSession, it.lastReceivedMessageTs) - } - null - } - } - - override fun getLastUsedSessionId(deviceKey: String): String? { - return doWithRealm(realmConfiguration) { - it.where() - .equalTo(OlmSessionEntityFields.DEVICE_KEY, deviceKey) - .sort(OlmSessionEntityFields.LAST_RECEIVED_MESSAGE_TS, Sort.DESCENDING) - .findFirst() - ?.sessionId - } - } - - override fun getDeviceSessionIds(deviceKey: String): List { - return doWithRealm(realmConfiguration) { - it.where() - .equalTo(OlmSessionEntityFields.DEVICE_KEY, deviceKey) - .sort(OlmSessionEntityFields.LAST_RECEIVED_MESSAGE_TS, Sort.DESCENDING) - .findAll() - .mapNotNull { sessionEntity -> - sessionEntity.sessionId - } - } - } - - override fun storeInboundGroupSessions(sessions: List) { - if (sessions.isEmpty()) { - return - } - - doRealmTransaction("storeInboundGroupSessions", realmConfiguration) { realm -> - sessions.forEach { wrapper -> - - val sessionIdentifier = try { - wrapper.session.sessionIdentifier() - } catch (e: OlmException) { - Timber.e(e, "## storeInboundGroupSession() : sessionIdentifier failed") - return@forEach - } - -// val shouldShareHistory = session.roomId?.let { roomId -> -// CryptoRoomEntity.getById(realm, roomId)?.shouldShareHistory -// } ?: false - val key = OlmInboundGroupSessionEntity.createPrimaryKey(sessionIdentifier, wrapper.sessionData.senderKey) - - val existing = realm.where() - .equalTo(OlmInboundGroupSessionEntityFields.PRIMARY_KEY, key) - .findFirst() - - val realmOlmInboundGroupSession = OlmInboundGroupSessionEntity().apply { - primaryKey = key - store(wrapper) - backedUp = existing?.backedUp ?: false - } - - Timber.v("## CRYPTO | shouldShareHistory: ${wrapper.sessionData.sharedHistory} for $key") - realm.insertOrUpdate(realmOlmInboundGroupSession) - } - } - } - - override fun getInboundGroupSession(sessionId: String, senderKey: String): MXInboundMegolmSessionWrapper? { - val key = OlmInboundGroupSessionEntity.createPrimaryKey(sessionId, senderKey) - - return doWithRealm(realmConfiguration) { realm -> - realm.where() - .equalTo(OlmInboundGroupSessionEntityFields.PRIMARY_KEY, key) - .findFirst() - ?.toModel() - } - } - - override fun getInboundGroupSession(sessionId: String, senderKey: String, sharedHistory: Boolean): MXInboundMegolmSessionWrapper? { - val key = OlmInboundGroupSessionEntity.createPrimaryKey(sessionId, senderKey) - return doWithRealm(realmConfiguration) { - it.where() - .equalTo(OlmInboundGroupSessionEntityFields.SHARED_HISTORY, sharedHistory) - .equalTo(OlmInboundGroupSessionEntityFields.PRIMARY_KEY, key) - .findFirst() - ?.toModel() - } - } - - override fun getCurrentOutboundGroupSessionForRoom(roomId: String): OutboundGroupSessionWrapper? { - return doWithRealm(realmConfiguration) { realm -> - realm.where() - .equalTo(CryptoRoomEntityFields.ROOM_ID, roomId) - .findFirst()?.outboundSessionInfo?.let { entity -> - entity.getOutboundGroupSession()?.let { - OutboundGroupSessionWrapper( - it, - entity.creationTime ?: 0, - entity.shouldShareHistory - ) - } - } - } - } - - override fun storeCurrentOutboundGroupSessionForRoom(roomId: String, outboundGroupSession: OlmOutboundGroupSession?) { - // we can do this async, as it's just for restoring on next launch - // the olmdevice is caching the active instance - // this is called for each sent message (so not high frequency), thus we can use basic realm async without - // risk of reaching max async operation limit? - doRealmTransactionAsync(realmConfiguration) { realm -> - CryptoRoomEntity.getById(realm, roomId)?.let { entity -> - // we should delete existing outbound session info if any - entity.outboundSessionInfo?.deleteFromRealm() - - if (outboundGroupSession != null) { - val info = realm.createObject(OutboundGroupSessionInfoEntity::class.java).apply { - creationTime = clock.epochMillis() - // Store the room history visibility on the outbound session creation - shouldShareHistory = entity.shouldShareHistory - putOutboundGroupSession(outboundGroupSession) - } - entity.outboundSessionInfo = info - } - } - } - } - -// override fun needsRotationDueToVisibilityChange(roomId: String): Boolean { -// return doWithRealm(realmConfiguration) { realm -> -// CryptoRoomEntity.getById(realm, roomId)?.let { entity -> -// entity.shouldShareHistory != entity.outboundSessionInfo?.shouldShareHistory -// } -// } ?: false -// } - - /** - * Note: the result will be only use to export all the keys and not to use the OlmInboundGroupSessionWrapper2, - * so there is no need to use or update `inboundGroupSessionToRelease` for native memory management. - */ - override fun getInboundGroupSessions(): List { - return doWithRealm(realmConfiguration) { realm -> - realm.where() - .findAll() - .mapNotNull { it.toModel() } - } - } - - override fun getInboundGroupSessions(roomId: String): List { - return doWithRealm(realmConfiguration) { realm -> - realm.where() - .equalTo(OlmInboundGroupSessionEntityFields.ROOM_ID, roomId) - .findAll() - .mapNotNull { it.toModel() } - } - } - - override fun removeInboundGroupSession(sessionId: String, senderKey: String) { - val key = OlmInboundGroupSessionEntity.createPrimaryKey(sessionId, senderKey) - - doRealmTransaction("removeInboundGroupSession", realmConfiguration) { - it.where() - .equalTo(OlmInboundGroupSessionEntityFields.PRIMARY_KEY, key) - .findAll() - .deleteAllFromRealm() - } - } - - /* ========================================================================================== - * Keys backup - * ========================================================================================== */ - - override fun getKeyBackupVersion(): String? { - return doRealmQueryAndCopy(realmConfiguration) { - it.where().findFirst() - }?.backupVersion - } - - override fun setKeyBackupVersion(keyBackupVersion: String?) { - doRealmTransaction("setKeyBackupVersion", realmConfiguration) { - it.where().findFirst()?.backupVersion = keyBackupVersion - } - } - - override fun getKeysBackupData(): KeysBackupDataEntity? { - return doRealmQueryAndCopy(realmConfiguration) { - it.where().findFirst() - } - } - - override fun setKeysBackupData(keysBackupData: KeysBackupDataEntity?) { - doRealmTransaction("setKeysBackupData", realmConfiguration) { - if (keysBackupData == null) { - // Clear the table - it.where() - .findAll() - .deleteAllFromRealm() - } else { - // Only one object - it.copyToRealmOrUpdate(keysBackupData) - } - } - } - - override fun resetBackupMarkers() { - doRealmTransaction("resetBackupMarkers", realmConfiguration) { - it.where() - .findAll() - .map { inboundGroupSession -> - inboundGroupSession.backedUp = false - } - } - } - - override fun markBackupDoneForInboundGroupSessions(olmInboundGroupSessionWrappers: List) { - if (olmInboundGroupSessionWrappers.isEmpty()) { - return - } - - doRealmTransaction("markBackupDoneForInboundGroupSessions", realmConfiguration) { realm -> - olmInboundGroupSessionWrappers.forEach { olmInboundGroupSessionWrapper -> - try { - val sessionIdentifier = - tryOrNull("Failed to get session identifier") { - olmInboundGroupSessionWrapper.session.sessionIdentifier() - } ?: return@forEach - val key = OlmInboundGroupSessionEntity.createPrimaryKey( - sessionIdentifier, - olmInboundGroupSessionWrapper.sessionData.senderKey - ) - - val existing = realm.where() - .equalTo(OlmInboundGroupSessionEntityFields.PRIMARY_KEY, key) - .findFirst() - - if (existing != null) { - existing.backedUp = true - } else { - // ... might be in cache but not yet persisted, create a record to persist backedup state - val realmOlmInboundGroupSession = OlmInboundGroupSessionEntity().apply { - primaryKey = key - store(olmInboundGroupSessionWrapper) - backedUp = true - } - - realm.insertOrUpdate(realmOlmInboundGroupSession) - } - } catch (e: OlmException) { - Timber.e(e, "OlmException") - } - } - } - } - - override fun inboundGroupSessionsToBackup(limit: Int): List { - return doWithRealm(realmConfiguration) { - it.where() - .equalTo(OlmInboundGroupSessionEntityFields.BACKED_UP, false) - .limit(limit.toLong()) - .findAll() - .mapNotNull { it.toModel() } - } - } - - override fun inboundGroupSessionsCount(onlyBackedUp: Boolean): Int { - return doWithRealm(realmConfiguration) { - it.where() - .apply { - if (onlyBackedUp) { - equalTo(OlmInboundGroupSessionEntityFields.BACKED_UP, true) - } - } - .count() - .toInt() - } - } - - override fun setGlobalBlacklistUnverifiedDevices(block: Boolean) { - doRealmTransaction("setGlobalBlacklistUnverifiedDevices", realmConfiguration) { - it.where().findFirst()?.globalBlacklistUnverifiedDevices = block - } - } - - override fun enableKeyGossiping(enable: Boolean) { - doRealmTransaction("enableKeyGossiping", realmConfiguration) { - it.where().findFirst()?.globalEnableKeyGossiping = enable - } - } - - override fun isKeyGossipingEnabled(): Boolean { - return doWithRealm(realmConfiguration) { - it.where().findFirst()?.globalEnableKeyGossiping - } ?: true - } - - override fun getGlobalBlacklistUnverifiedDevices(): Boolean { - return doWithRealm(realmConfiguration) { - it.where().findFirst()?.globalBlacklistUnverifiedDevices - } ?: false - } - - override fun isShareKeysOnInviteEnabled(): Boolean { - return doWithRealm(realmConfiguration) { - it.where().findFirst()?.enableKeyForwardingOnInvite - } ?: false - } - - override fun enableShareKeyOnInvite(enable: Boolean) { - doRealmTransaction("enableShareKeyOnInvite", realmConfiguration) { - it.where().findFirst()?.enableKeyForwardingOnInvite = enable - } - } - - override fun setDeviceKeysUploaded(uploaded: Boolean) { - doRealmTransaction("setDeviceKeysUploaded", realmConfiguration) { - it.where().findFirst()?.deviceKeysSentToServer = uploaded - } - } - - override fun areDeviceKeysUploaded(): Boolean { - return doWithRealm(realmConfiguration) { - it.where().findFirst()?.deviceKeysSentToServer - } ?: false - } - - override fun getRoomsListBlacklistUnverifiedDevices(): List { - return doWithRealm(realmConfiguration) { - it.where() - .equalTo(CryptoRoomEntityFields.BLACKLIST_UNVERIFIED_DEVICES, true) - .findAll() - .mapNotNull { cryptoRoom -> - cryptoRoom.roomId - } - } - } - - override fun getLiveBlockUnverifiedDevices(roomId: String): LiveData { - val liveData = monarchy.findAllMappedWithChanges( - { realm: Realm -> - realm.where() - .equalTo(CryptoRoomEntityFields.ROOM_ID, roomId) - }, - { - it.blacklistUnverifiedDevices - } - ) - return Transformations.map(liveData) { - it.firstOrNull() ?: false - } - } - - override fun getBlockUnverifiedDevices(roomId: String): Boolean { - return doWithRealm(realmConfiguration) { realm -> - realm.where() - .equalTo(CryptoRoomEntityFields.ROOM_ID, roomId) - .findFirst() - ?.blacklistUnverifiedDevices ?: false - } - } - - override fun blockUnverifiedDevicesInRoom(roomId: String, block: Boolean) { - doRealmTransaction("blockUnverifiedDevicesInRoom", realmConfiguration) { realm -> - CryptoRoomEntity.getById(realm, roomId) - ?.blacklistUnverifiedDevices = block - } - } - - override fun getDeviceTrackingStatuses(): Map { - return doWithRealm(realmConfiguration) { - it.where() - .findAll() - .associateBy { user -> - user.userId!! - } - .mapValues { entry -> - entry.value.deviceTrackingStatus - } - } - } - - override fun saveDeviceTrackingStatuses(deviceTrackingStatuses: Map) { - doRealmTransaction("saveDeviceTrackingStatuses", realmConfiguration) { - deviceTrackingStatuses - .map { entry -> - UserEntity.getOrCreate(it, entry.key) - .deviceTrackingStatus = entry.value - } - } - } - - override fun getDeviceTrackingStatus(userId: String, defaultValue: Int): Int { - return doWithRealm(realmConfiguration) { - it.where() - .equalTo(UserEntityFields.USER_ID, userId) - .findFirst() - ?.deviceTrackingStatus - } - ?: defaultValue - } - - override fun getOutgoingRoomKeyRequest(requestBody: RoomKeyRequestBody): OutgoingKeyRequest? { - return monarchy.fetchAllCopiedSync { realm -> - realm.where() - .equalTo(OutgoingKeyRequestEntityFields.ROOM_ID, requestBody.roomId) - .equalTo(OutgoingKeyRequestEntityFields.MEGOLM_SESSION_ID, requestBody.sessionId) - }.map { - it.toOutgoingKeyRequest() - }.firstOrNull { - it.requestBody?.algorithm == requestBody.algorithm && - it.requestBody?.roomId == requestBody.roomId && - it.requestBody?.senderKey == requestBody.senderKey && - it.requestBody?.sessionId == requestBody.sessionId - } - } - - override fun getOutgoingRoomKeyRequest(requestId: String): OutgoingKeyRequest? { - return monarchy.fetchAllCopiedSync { realm -> - realm.where() - .equalTo(OutgoingKeyRequestEntityFields.REQUEST_ID, requestId) - }.map { - it.toOutgoingKeyRequest() - }.firstOrNull() - } - - override fun getOutgoingRoomKeyRequest(roomId: String, sessionId: String, algorithm: String, senderKey: String): List { - // TODO this annoying we have to load all - return monarchy.fetchAllCopiedSync { realm -> - realm.where() - .equalTo(OutgoingKeyRequestEntityFields.ROOM_ID, roomId) - .equalTo(OutgoingKeyRequestEntityFields.MEGOLM_SESSION_ID, sessionId) - }.map { - it.toOutgoingKeyRequest() - }.filter { - it.requestBody?.algorithm == algorithm && - it.requestBody?.senderKey == senderKey - } - } - - override fun getGossipingEventsTrail(): LiveData> { - val realmDataSourceFactory = monarchy.createDataSourceFactory { realm -> - realm.where().sort(AuditTrailEntityFields.AGE_LOCAL_TS, Sort.DESCENDING) - } - val dataSourceFactory = realmDataSourceFactory.map { - AuditTrailMapper.map(it) - // mm we can't map not null... - ?: createUnknownTrail() - } - return monarchy.findAllPagedWithChanges( - realmDataSourceFactory, - LivePagedListBuilder( - dataSourceFactory, - PagedList.Config.Builder() - .setPageSize(20) - .setEnablePlaceholders(false) - .setPrefetchDistance(1) - .build() - ) - ) - } - - private fun createUnknownTrail() = AuditTrail( - clock.epochMillis(), - TrailType.Unknown, - IncomingKeyRequestInfo( - "", - "", - "", - "", - "", - "", - "", - ) - ) - - override fun getGossipingEventsTrail(type: TrailType, mapper: ((AuditTrail) -> T)): LiveData> { - val realmDataSourceFactory = monarchy.createDataSourceFactory { realm -> - realm.where() - .equalTo(AuditTrailEntityFields.TYPE, type.name) - .sort(AuditTrailEntityFields.AGE_LOCAL_TS, Sort.DESCENDING) - } - val dataSourceFactory = realmDataSourceFactory.map { entity -> - (AuditTrailMapper.map(entity) - // mm we can't map not null... - ?: createUnknownTrail() - ).let { mapper.invoke(it) } - } - return monarchy.findAllPagedWithChanges( - realmDataSourceFactory, - LivePagedListBuilder( - dataSourceFactory, - PagedList.Config.Builder() - .setPageSize(20) - .setEnablePlaceholders(false) - .setPrefetchDistance(1) - .build() - ) - ) - } - - override fun getGossipingEvents(): List { - return monarchy.fetchAllCopiedSync { realm -> - realm.where() - }.mapNotNull { - AuditTrailMapper.map(it) - } - } - - override fun getOrAddOutgoingRoomKeyRequest( - requestBody: RoomKeyRequestBody, - recipients: Map>, - fromIndex: Int - ): OutgoingKeyRequest { - // Insert the request and return the one passed in parameter - lateinit var request: OutgoingKeyRequest - doRealmTransaction("getOrAddOutgoingRoomKeyRequest", realmConfiguration) { realm -> - - val existing = realm.where() - .equalTo(OutgoingKeyRequestEntityFields.MEGOLM_SESSION_ID, requestBody.sessionId) - .equalTo(OutgoingKeyRequestEntityFields.ROOM_ID, requestBody.roomId) - .findAll() - .map { - it.toOutgoingKeyRequest() - }.also { - if (it.size > 1) { - // there should be one or zero but not more, worth warning - Timber.tag(loggerTag.value).w("There should not be more than one active key request per session") - } - } - .firstOrNull { - it.requestBody?.algorithm == requestBody.algorithm && - it.requestBody?.sessionId == requestBody.sessionId && - it.requestBody?.senderKey == requestBody.senderKey && - it.requestBody?.roomId == requestBody.roomId - } - - if (existing == null) { - request = realm.createObject(OutgoingKeyRequestEntity::class.java).apply { - this.requestId = RequestIdHelper.createUniqueRequestId() - this.setRecipients(recipients) - this.requestedIndex = fromIndex - this.requestState = OutgoingRoomKeyRequestState.UNSENT - this.setRequestBody(requestBody) - this.creationTimeStamp = clock.epochMillis() - }.toOutgoingKeyRequest() - } else { - request = existing - } - } - return request - } - - override fun updateOutgoingRoomKeyRequestState(requestId: String, newState: OutgoingRoomKeyRequestState) { - doRealmTransaction("updateOutgoingRoomKeyRequestState", realmConfiguration) { realm -> - realm.where() - .equalTo(OutgoingKeyRequestEntityFields.REQUEST_ID, requestId) - .findFirst()?.apply { - this.requestState = newState - if (newState == OutgoingRoomKeyRequestState.UNSENT) { - // clear the old replies - this.replies.deleteAllFromRealm() - } - } - } - } - - override fun updateOutgoingRoomKeyRequiredIndex(requestId: String, newIndex: Int) { - doRealmTransaction("updateOutgoingRoomKeyRequiredIndex", realmConfiguration) { realm -> - realm.where() - .equalTo(OutgoingKeyRequestEntityFields.REQUEST_ID, requestId) - .findFirst()?.apply { - this.requestedIndex = newIndex - } - } - } - - override fun updateOutgoingRoomKeyReply( - roomId: String, - sessionId: String, - algorithm: String, - senderKey: String, - fromDevice: String?, - event: Event - ) { - doRealmTransaction("updateOutgoingRoomKeyReply", realmConfiguration) { realm -> - realm.where() - .equalTo(OutgoingKeyRequestEntityFields.ROOM_ID, roomId) - .equalTo(OutgoingKeyRequestEntityFields.MEGOLM_SESSION_ID, sessionId) - .findAll().firstOrNull { entity -> - entity.toOutgoingKeyRequest().let { - it.requestBody?.senderKey == senderKey && - it.requestBody?.algorithm == algorithm - } - }?.apply { - event.senderId?.let { addReply(it, fromDevice, event) } - } - } - } - - override fun deleteOutgoingRoomKeyRequest(requestId: String) { - doRealmTransaction("deleteOutgoingRoomKeyRequest", realmConfiguration) { realm -> - realm.where() - .equalTo(OutgoingKeyRequestEntityFields.REQUEST_ID, requestId) - .findFirst()?.deleteOnCascade() - } - } - - override fun deleteOutgoingRoomKeyRequestInState(state: OutgoingRoomKeyRequestState) { - doRealmTransaction("deleteOutgoingRoomKeyRequestInState", realmConfiguration) { realm -> - realm.where() - .equalTo(OutgoingKeyRequestEntityFields.REQUEST_STATE_STR, state.name) - .findAll() - // I delete like this because I want to cascade delete replies? - .onEach { it.deleteOnCascade() } - } - } - - override fun saveIncomingKeyRequestAuditTrail( - requestId: String, - roomId: String, - sessionId: String, - senderKey: String, - algorithm: String, - fromUser: String, - fromDevice: String - ) { - monarchy.writeAsync { realm -> - val now = clock.epochMillis() - realm.createObject().apply { - this.ageLocalTs = now - this.type = TrailType.IncomingKeyRequest.name - val info = IncomingKeyRequestInfo( - roomId = roomId, - sessionId = sessionId, - senderKey = senderKey, - alg = algorithm, - userId = fromUser, - deviceId = fromDevice, - requestId = requestId - ) - MoshiProvider.providesMoshi().adapter(IncomingKeyRequestInfo::class.java).toJson(info)?.let { - this.contentJson = it - } - } - } - } - - override fun saveWithheldAuditTrail( - roomId: String, - sessionId: String, - senderKey: String, - algorithm: String, - code: WithHeldCode, - userId: String, - deviceId: String - ) { - monarchy.writeAsync { realm -> - val now = clock.epochMillis() - realm.createObject().apply { - this.ageLocalTs = now - this.type = TrailType.OutgoingKeyWithheld.name - val info = WithheldInfo( - roomId = roomId, - sessionId = sessionId, - senderKey = senderKey, - alg = algorithm, - code = code, - userId = userId, - deviceId = deviceId - ) - MoshiProvider.providesMoshi().adapter(WithheldInfo::class.java).toJson(info)?.let { - this.contentJson = it - } - } - } - } - - override fun saveForwardKeyAuditTrail( - roomId: String, - sessionId: String, - senderKey: String, - algorithm: String, - userId: String, - deviceId: String, - chainIndex: Long? - ) { - saveForwardKeyTrail(roomId, sessionId, senderKey, algorithm, userId, deviceId, chainIndex, false) - } - - override fun saveIncomingForwardKeyAuditTrail( - roomId: String, - sessionId: String, - senderKey: String, - algorithm: String, - userId: String, - deviceId: String, - chainIndex: Long? - ) { - saveForwardKeyTrail(roomId, sessionId, senderKey, algorithm, userId, deviceId, chainIndex, true) - } - - private fun saveForwardKeyTrail( - roomId: String, - sessionId: String, - senderKey: String, - algorithm: String, - userId: String, - deviceId: String, - chainIndex: Long?, - incoming: Boolean - ) { - monarchy.writeAsync { realm -> - val now = clock.epochMillis() - realm.createObject().apply { - this.ageLocalTs = now - this.type = if (incoming) TrailType.IncomingKeyForward.name else TrailType.OutgoingKeyForward.name - val info = ForwardInfo( - roomId = roomId, - sessionId = sessionId, - senderKey = senderKey, - alg = algorithm, - userId = userId, - deviceId = deviceId, - chainIndex = chainIndex - ) - MoshiProvider.providesMoshi().adapter(ForwardInfo::class.java).toJson(info)?.let { - this.contentJson = it - } - } - } - } - - /* ========================================================================================== - * Cross Signing - * ========================================================================================== */ - override fun getMyCrossSigningInfo(): MXCrossSigningInfo? { - return doWithRealm(realmConfiguration) { - it.where().findFirst()?.userId - }?.let { - getCrossSigningInfo(it) - } - } - - override fun setMyCrossSigningInfo(info: MXCrossSigningInfo?) { - doRealmTransaction("setMyCrossSigningInfo", realmConfiguration) { realm -> - realm.where().findFirst()?.userId?.let { userId -> - addOrUpdateCrossSigningInfo(realm, userId, info) - } - } - } - - override fun setUserKeysAsTrusted(userId: String, trusted: Boolean) { - doRealmTransaction("setUserKeysAsTrusted", realmConfiguration) { realm -> - val xInfoEntity = realm.where(CrossSigningInfoEntity::class.java) - .equalTo(CrossSigningInfoEntityFields.USER_ID, userId) - .findFirst() - xInfoEntity?.crossSigningKeys?.forEach { info -> - val level = info.trustLevelEntity - if (level == null) { - val newLevel = realm.createObject(TrustLevelEntity::class.java) - newLevel.locallyVerified = trusted - newLevel.crossSignedVerified = trusted - info.trustLevelEntity = newLevel - } else { - level.locallyVerified = trusted - level.crossSignedVerified = trusted - } - } - } - } - - override fun setDeviceTrust(userId: String, deviceId: String, crossSignedVerified: Boolean, locallyVerified: Boolean?) { - doRealmTransaction("setDeviceTrust", realmConfiguration) { realm -> - realm.where(DeviceInfoEntity::class.java) - .equalTo(DeviceInfoEntityFields.PRIMARY_KEY, DeviceInfoEntity.createPrimaryKey(userId, deviceId)) - .findFirst()?.let { deviceInfoEntity -> - val trustEntity = deviceInfoEntity.trustLevelEntity - if (trustEntity == null) { - realm.createObject(TrustLevelEntity::class.java).let { - it.locallyVerified = locallyVerified - it.crossSignedVerified = crossSignedVerified - deviceInfoEntity.trustLevelEntity = it - } - } else { - locallyVerified?.let { trustEntity.locallyVerified = it } - trustEntity.crossSignedVerified = crossSignedVerified - } - } - } - } - - override fun clearOtherUserTrust() { - doRealmTransaction("clearOtherUserTrust", realmConfiguration) { realm -> - val xInfoEntities = realm.where(CrossSigningInfoEntity::class.java) - .findAll() - xInfoEntities?.forEach { info -> - // Need to ignore mine - if (info.userId != userId) { - info.crossSigningKeys.forEach { - it.trustLevelEntity = null - } - } - } - } - } - - override fun updateUsersTrust(check: (String) -> Boolean) { - doRealmTransaction("updateUsersTrust", realmConfiguration) { realm -> - val xInfoEntities = realm.where(CrossSigningInfoEntity::class.java) - .findAll() - xInfoEntities?.forEach { xInfoEntity -> - // Need to ignore mine - if (xInfoEntity.userId == userId) return@forEach - val mapped = mapCrossSigningInfoEntity(xInfoEntity) - val currentTrust = mapped.isTrusted() - val newTrust = check(mapped.userId) - if (currentTrust != newTrust) { - xInfoEntity.crossSigningKeys.forEach { info -> - val level = info.trustLevelEntity - if (level == null) { - val newLevel = realm.createObject(TrustLevelEntity::class.java) - newLevel.locallyVerified = newTrust - newLevel.crossSignedVerified = newTrust - info.trustLevelEntity = newLevel - } else { - level.locallyVerified = newTrust - level.crossSignedVerified = newTrust - } - } - } - } - } - } - - override fun getOutgoingRoomKeyRequests(): List { - return monarchy.fetchAllMappedSync({ realm -> - realm - .where(OutgoingKeyRequestEntity::class.java) - }, { entity -> - entity.toOutgoingKeyRequest() - }) - .filterNotNull() - } - - override fun getOutgoingRoomKeyRequests(inStates: Set): List { - return monarchy.fetchAllMappedSync({ realm -> - realm - .where(OutgoingKeyRequestEntity::class.java) - .`in`(OutgoingKeyRequestEntityFields.REQUEST_STATE_STR, inStates.map { it.name }.toTypedArray()) - }, { entity -> - entity.toOutgoingKeyRequest() - }) - .filterNotNull() - } - - override fun getOutgoingRoomKeyRequestsPaged(): LiveData> { - val realmDataSourceFactory = monarchy.createDataSourceFactory { realm -> - realm - .where(OutgoingKeyRequestEntity::class.java) - } - val dataSourceFactory = realmDataSourceFactory.map { - it.toOutgoingKeyRequest() - } - val trail = monarchy.findAllPagedWithChanges( - realmDataSourceFactory, - LivePagedListBuilder( - dataSourceFactory, - PagedList.Config.Builder() - .setPageSize(20) - .setEnablePlaceholders(false) - .setPrefetchDistance(1) - .build() - ) - ) - return trail - } - - override fun getCrossSigningInfo(userId: String): MXCrossSigningInfo? { - return doWithRealm(realmConfiguration) { realm -> - val crossSigningInfo = realm.where(CrossSigningInfoEntity::class.java) - .equalTo(CrossSigningInfoEntityFields.USER_ID, userId) - .findFirst() - if (crossSigningInfo == null) { - null - } else { - mapCrossSigningInfoEntity(crossSigningInfo) - } - } - } - - private fun mapCrossSigningInfoEntity(xsignInfo: CrossSigningInfoEntity): MXCrossSigningInfo { - val userId = xsignInfo.userId ?: "" - return MXCrossSigningInfo( - userId = userId, - crossSigningKeys = xsignInfo.crossSigningKeys.mapNotNull { - crossSigningKeysMapper.map(userId, it) - }, - wasTrustedOnce = xsignInfo.wasUserVerifiedOnce - ) - } - - override fun setCrossSigningInfo(userId: String, info: MXCrossSigningInfo?) { - doRealmTransaction("setCrossSigningInfo", realmConfiguration) { realm -> - addOrUpdateCrossSigningInfo(realm, userId, info) - } - } - - override fun markMyMasterKeyAsLocallyTrusted(trusted: Boolean) { - doRealmTransaction("markMyMasterKeyAsLocallyTrusted", realmConfiguration) { realm -> - realm.where().findFirst()?.userId?.let { myUserId -> - CrossSigningInfoEntity.get(realm, myUserId)?.getMasterKey()?.let { xInfoEntity -> - val level = xInfoEntity.trustLevelEntity - if (level == null) { - val newLevel = realm.createObject(TrustLevelEntity::class.java) - newLevel.locallyVerified = trusted - xInfoEntity.trustLevelEntity = newLevel - } else { - level.locallyVerified = trusted - } - } - } - } - } - - private fun addOrUpdateCrossSigningInfo(realm: Realm, userId: String, info: MXCrossSigningInfo?): CrossSigningInfoEntity? { - if (info == null) { - // Delete known if needed - CrossSigningInfoEntity.get(realm, userId)?.deleteFromRealm() - return null - // TODO notify, we might need to untrust things? - } else { - // Just override existing, caller should check and untrust id needed - val existing = CrossSigningInfoEntity.getOrCreate(realm, userId) - existing.crossSigningKeys.clearWith { it.deleteOnCascade() } - existing.crossSigningKeys.addAll( - info.crossSigningKeys.map { - crossSigningKeysMapper.map(it) - } - ) - return existing - } - } - - override fun addWithHeldMegolmSession(withHeldContent: RoomKeyWithHeldContent) { - val roomId = withHeldContent.roomId ?: return - val sessionId = withHeldContent.sessionId ?: return - if (withHeldContent.algorithm != MXCRYPTO_ALGORITHM_MEGOLM) return - doRealmTransaction("addWithHeldMegolmSession", realmConfiguration) { realm -> - WithHeldSessionEntity.getOrCreate(realm, roomId, sessionId)?.let { - it.code = withHeldContent.code - it.senderKey = withHeldContent.senderKey - it.reason = withHeldContent.reason - } - } - } - - override fun getWithHeldMegolmSession(roomId: String, sessionId: String): RoomKeyWithHeldContent? { - return doWithRealm(realmConfiguration) { realm -> - WithHeldSessionEntity.get(realm, roomId, sessionId)?.let { - RoomKeyWithHeldContent( - roomId = roomId, - sessionId = sessionId, - algorithm = it.algorithm, - codeString = it.codeString, - reason = it.reason, - senderKey = it.senderKey - ) - } - } - } - - override fun markedSessionAsShared( - roomId: String?, - sessionId: String, - userId: String, - deviceId: String, - deviceIdentityKey: String, - chainIndex: Int - ) { - doRealmTransaction("markedSessionAsShared", realmConfiguration) { realm -> - SharedSessionEntity.create( - realm = realm, - roomId = roomId, - sessionId = sessionId, - userId = userId, - deviceId = deviceId, - deviceIdentityKey = deviceIdentityKey, - chainIndex = chainIndex - ) - } - } - - override fun getSharedSessionInfo(roomId: String?, sessionId: String, deviceInfo: CryptoDeviceInfo): IMXCryptoStore.SharedSessionResult { - return doWithRealm(realmConfiguration) { realm -> - SharedSessionEntity.get( - realm = realm, - roomId = roomId, - sessionId = sessionId, - userId = deviceInfo.userId, - deviceId = deviceInfo.deviceId, - deviceIdentityKey = deviceInfo.identityKey() - )?.let { - IMXCryptoStore.SharedSessionResult(true, it.chainIndex) - } ?: IMXCryptoStore.SharedSessionResult(false, null) - } - } - - override fun getSharedWithInfo(roomId: String?, sessionId: String): MXUsersDevicesMap { - return doWithRealm(realmConfiguration) { realm -> - val result = MXUsersDevicesMap() - SharedSessionEntity.get(realm, roomId, sessionId) - .groupBy { it.userId } - .forEach { (userId, shared) -> - shared.forEach { - result.setObject(userId, it.deviceId, it.chainIndex) - } - } - - result - } - } - - /** - * Some entries in the DB can get a bit out of control with time. - * So we need to tidy up a bit. - */ - override fun tidyUpDataBase() { - val prevWeekTs = clock.epochMillis() - 7 * 24 * 60 * 60 * 1_000 - doRealmTransaction("tidyUpDataBase", realmConfiguration) { realm -> - - // Clean the old ones? - realm.where() - .lessThan(OutgoingKeyRequestEntityFields.CREATION_TIME_STAMP, prevWeekTs) - .findAll() - .also { Timber.i("## Crypto Clean up ${it.size} OutgoingKeyRequestEntity") } - .deleteAllFromRealm() - - // Only keep one month history - - val prevMonthTs = clock.epochMillis() - 4 * 7 * 24 * 60 * 60 * 1_000L - realm.where() - .lessThan(AuditTrailEntityFields.AGE_LOCAL_TS, prevMonthTs) - .findAll() - .also { Timber.i("## Crypto Clean up ${it.size} AuditTrailEntity") } - .deleteAllFromRealm() - - // Can we do something for WithHeldSessionEntity? - } - } - - override fun storeData(cryptoStoreAggregator: CryptoStoreAggregator) { - if (cryptoStoreAggregator.isEmpty()) { - return - } - doRealmTransaction("storeData - CryptoStoreAggregator", realmConfiguration) { realm -> - // setShouldShareHistory - cryptoStoreAggregator.setShouldShareHistoryData.forEach { - Timber.tag(loggerTag.value) - .v("setShouldShareHistory for room ${it.key} is ${it.value}") - CryptoRoomEntity.getOrCreate(realm, it.key).shouldShareHistory = it.value - } - // setShouldEncryptForInvitedMembers - cryptoStoreAggregator.setShouldEncryptForInvitedMembersData.forEach { - CryptoRoomEntity.getOrCreate(realm, it.key).shouldEncryptForInvitedMembers = it.value - } - } - } - - override fun storeData(userDataToStore: UserDataToStore) { - doRealmTransaction("storeData - UserDataToStore", realmConfiguration) { realm -> - userDataToStore.userDevices.forEach { - storeUserDevices(realm, it.key, it.value) - } - userDataToStore.userIdentities.forEach { - storeUserIdentity(realm, it.key, it.value) - } - } - } -} diff --git a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStoreMigration.kt b/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStoreMigration.kt deleted file mode 100644 index c1aeff368f..0000000000 --- a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStoreMigration.kt +++ /dev/null @@ -1,87 +0,0 @@ -/* - * Copyright 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. - */ - -package org.matrix.android.sdk.internal.crypto.store.db - -import io.realm.DynamicRealm -import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo001Legacy -import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo002Legacy -import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo003RiotX -import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo004 -import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo005 -import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo006 -import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo007 -import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo008 -import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo009 -import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo010 -import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo011 -import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo012 -import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo013 -import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo014 -import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo015 -import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo016 -import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo017 -import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo018 -import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo019 -import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo020 -import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo021 -import org.matrix.android.sdk.internal.util.database.MatrixRealmMigration -import org.matrix.android.sdk.internal.util.time.Clock -import javax.inject.Inject - -/** - * Schema version history: - * 0, 1, 2: legacy Riot-Android; - * 3: migrate to RiotX schema; - * 4, 5, 6, 7, 8, 9: migrations from RiotX (which was previously 1, 2, 3, 4, 5, 6). - */ -internal class RealmCryptoStoreMigration @Inject constructor( - private val clock: Clock, -) : MatrixRealmMigration( - dbName = "Crypto", - schemaVersion = 21L, -) { - /** - * Forces all RealmCryptoStoreMigration instances to be equal. - * Avoids Realm throwing when multiple instances of the migration are set. - */ - override fun equals(other: Any?) = other is RealmCryptoStoreMigration - override fun hashCode() = 5000 - - override fun doMigrate(realm: DynamicRealm, oldVersion: Long) { - if (oldVersion < 1) MigrateCryptoTo001Legacy(realm).perform() - if (oldVersion < 2) MigrateCryptoTo002Legacy(realm).perform() - if (oldVersion < 3) MigrateCryptoTo003RiotX(realm).perform() - if (oldVersion < 4) MigrateCryptoTo004(realm).perform() - if (oldVersion < 5) MigrateCryptoTo005(realm).perform() - if (oldVersion < 6) MigrateCryptoTo006(realm).perform() - if (oldVersion < 7) MigrateCryptoTo007(realm).perform() - if (oldVersion < 8) MigrateCryptoTo008(realm, clock).perform() - if (oldVersion < 9) MigrateCryptoTo009(realm).perform() - if (oldVersion < 10) MigrateCryptoTo010(realm).perform() - if (oldVersion < 11) MigrateCryptoTo011(realm).perform() - if (oldVersion < 12) MigrateCryptoTo012(realm).perform() - if (oldVersion < 13) MigrateCryptoTo013(realm).perform() - if (oldVersion < 14) MigrateCryptoTo014(realm).perform() - if (oldVersion < 15) MigrateCryptoTo015(realm).perform() - if (oldVersion < 16) MigrateCryptoTo016(realm).perform() - if (oldVersion < 17) MigrateCryptoTo017(realm).perform() - if (oldVersion < 18) MigrateCryptoTo018(realm).perform() - if (oldVersion < 19) MigrateCryptoTo019(realm).perform() - if (oldVersion < 20) MigrateCryptoTo020(realm).perform() - if (oldVersion < 21) MigrateCryptoTo021(realm).perform() - } -} diff --git a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/task/InitializeCrossSigningTask.kt b/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/task/InitializeCrossSigningTask.kt deleted file mode 100644 index 53190c43ff..0000000000 --- a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/task/InitializeCrossSigningTask.kt +++ /dev/null @@ -1,190 +0,0 @@ -/* - * Copyright 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. - */ - -package org.matrix.android.sdk.internal.crypto.tasks - -import dagger.Lazy -import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor -import org.matrix.android.sdk.api.session.crypto.crosssigning.CryptoCrossSigningKey -import org.matrix.android.sdk.api.session.crypto.crosssigning.KeyUsage -import org.matrix.android.sdk.api.session.uia.UiaResult -import org.matrix.android.sdk.api.util.toBase64NoPadding -import org.matrix.android.sdk.internal.auth.registration.handleUIA -import org.matrix.android.sdk.internal.crypto.MXOlmDevice -import org.matrix.android.sdk.internal.crypto.MyDeviceInfoHolder -import org.matrix.android.sdk.internal.crypto.crosssigning.canonicalSignable -import org.matrix.android.sdk.internal.crypto.model.rest.UploadSignatureQueryBuilder -import org.matrix.android.sdk.internal.di.UserId -import org.matrix.android.sdk.internal.task.Task -import org.matrix.android.sdk.internal.util.JsonCanonicalizer -import org.matrix.olm.OlmPkSigning -import timber.log.Timber -import javax.inject.Inject - -internal interface InitializeCrossSigningTask : Task { - data class Params( - val interactiveAuthInterceptor: UserInteractiveAuthInterceptor? - ) - - data class Result( - val masterKeyPK: String, - val userKeyPK: String, - val selfSigningKeyPK: String, - val masterKeyInfo: CryptoCrossSigningKey, - val userKeyInfo: CryptoCrossSigningKey, - val selfSignedKeyInfo: CryptoCrossSigningKey - ) -} - -internal class DefaultInitializeCrossSigningTask @Inject constructor( - @UserId private val userId: String, - private val olmDevice: MXOlmDevice, - private val myDeviceInfoHolder: Lazy, - private val uploadSigningKeysTask: UploadSigningKeysTask, - private val uploadSignaturesTask: UploadSignaturesTask -) : InitializeCrossSigningTask { - - override suspend fun execute(params: InitializeCrossSigningTask.Params): InitializeCrossSigningTask.Result { - var masterPkOlm: OlmPkSigning? = null - var userSigningPkOlm: OlmPkSigning? = null - var selfSigningPkOlm: OlmPkSigning? = null - - try { - // ================= - // MASTER KEY - // ================= - - masterPkOlm = OlmPkSigning() - val masterKeyPrivateKey = OlmPkSigning.generateSeed() - val masterPublicKey = masterPkOlm.initWithSeed(masterKeyPrivateKey) - - Timber.v("## CrossSigning - masterPublicKey:$masterPublicKey") - - // ================= - // USER KEY - // ================= - userSigningPkOlm = OlmPkSigning() - val uskPrivateKey = OlmPkSigning.generateSeed() - val uskPublicKey = userSigningPkOlm.initWithSeed(uskPrivateKey) - - Timber.v("## CrossSigning - uskPublicKey:$uskPublicKey") - - // Sign userSigningKey with master - val signedUSK = CryptoCrossSigningKey.Builder(userId, KeyUsage.USER_SIGNING) - .key(uskPublicKey) - .build() - .canonicalSignable() - .let { masterPkOlm.sign(it) } - - // ================= - // SELF SIGNING KEY - // ================= - selfSigningPkOlm = OlmPkSigning() - val sskPrivateKey = OlmPkSigning.generateSeed() - val sskPublicKey = selfSigningPkOlm.initWithSeed(sskPrivateKey) - - Timber.v("## CrossSigning - sskPublicKey:$sskPublicKey") - - // Sign selfSigningKey with master - val signedSSK = CryptoCrossSigningKey.Builder(userId, KeyUsage.SELF_SIGNING) - .key(sskPublicKey) - .build() - .canonicalSignable() - .let { masterPkOlm.sign(it) } - - // I need to upload the keys - val mskCrossSigningKeyInfo = CryptoCrossSigningKey.Builder(userId, KeyUsage.MASTER) - .key(masterPublicKey) - .build() - val uploadSigningKeysParams = UploadSigningKeysTask.Params( - masterKey = mskCrossSigningKeyInfo, - userKey = CryptoCrossSigningKey.Builder(userId, KeyUsage.USER_SIGNING) - .key(uskPublicKey) - .signature(userId, masterPublicKey, signedUSK) - .build(), - selfSignedKey = CryptoCrossSigningKey.Builder(userId, KeyUsage.SELF_SIGNING) - .key(sskPublicKey) - .signature(userId, masterPublicKey, signedSSK) - .build(), - userAuthParam = null -// userAuthParam = params.authParams - ) - - try { - uploadSigningKeysTask.execute(uploadSigningKeysParams) - } catch (failure: Throwable) { - if (params.interactiveAuthInterceptor == null || - handleUIA( - failure = failure, - interceptor = params.interactiveAuthInterceptor, - retryBlock = { authUpdate -> - uploadSigningKeysTask.execute(uploadSigningKeysParams.copy(userAuthParam = authUpdate)) - } - ) != UiaResult.SUCCESS - ) { - Timber.d("## UIA: propagate failure") - throw failure - } - } - - // Sign the current device with SSK - val uploadSignatureQueryBuilder = UploadSignatureQueryBuilder() - - val myDevice = myDeviceInfoHolder.get().myDevice - val canonicalJson = JsonCanonicalizer.getCanonicalJson(Map::class.java, myDevice.signalableJSONDictionary()) - val signedDevice = selfSigningPkOlm.sign(canonicalJson) - val updateSignatures = (myDevice.signatures?.toMutableMap() ?: HashMap()) - .also { - it[userId] = (it[userId] - ?: HashMap()) + mapOf("ed25519:$sskPublicKey" to signedDevice) - } - myDevice.copy(signatures = updateSignatures).let { - uploadSignatureQueryBuilder.withDeviceInfo(it) - } - - // sign MSK with device key (migration) and upload signatures - val message = JsonCanonicalizer.getCanonicalJson(Map::class.java, mskCrossSigningKeyInfo.signalableJSONDictionary()) - olmDevice.signMessage(message)?.let { sign -> - val mskUpdatedSignatures = (mskCrossSigningKeyInfo.signatures?.toMutableMap() - ?: HashMap()).also { - it[userId] = (it[userId] - ?: HashMap()) + mapOf("ed25519:${myDevice.deviceId}" to sign) - } - mskCrossSigningKeyInfo.copy( - signatures = mskUpdatedSignatures - ).let { - uploadSignatureQueryBuilder.withSigningKeyInfo(it) - } - } - - // TODO should we ignore failure of that? - uploadSignaturesTask.execute(UploadSignaturesTask.Params(uploadSignatureQueryBuilder.build())) - - return InitializeCrossSigningTask.Result( - masterKeyPK = masterKeyPrivateKey.toBase64NoPadding(), - userKeyPK = uskPrivateKey.toBase64NoPadding(), - selfSigningKeyPK = sskPrivateKey.toBase64NoPadding(), - masterKeyInfo = uploadSigningKeysParams.masterKey, - userKeyInfo = uploadSigningKeysParams.userKey, - selfSignedKeyInfo = uploadSigningKeysParams.selfSignedKey - ) - } finally { - masterPkOlm?.releaseSigning() - userSigningPkOlm?.releaseSigning() - selfSigningPkOlm?.releaseSigning() - } - } -} diff --git a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/DefaultVerificationService.kt b/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/DefaultVerificationService.kt deleted file mode 100644 index 313d2bc265..0000000000 --- a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/DefaultVerificationService.kt +++ /dev/null @@ -1,1713 +0,0 @@ -/* - * Copyright 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. - */ - -package org.matrix.android.sdk.internal.crypto.verification - -import kotlinx.coroutines.CompletableDeferred -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.SupervisorJob -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.launch -import org.matrix.android.sdk.api.MatrixCoroutineDispatchers -import org.matrix.android.sdk.api.session.crypto.crosssigning.DeviceTrustLevel -import org.matrix.android.sdk.api.session.crypto.verification.PendingVerificationRequest -import org.matrix.android.sdk.api.session.crypto.verification.VerificationEvent -import org.matrix.android.sdk.api.session.crypto.verification.VerificationMethod -import org.matrix.android.sdk.api.session.crypto.verification.VerificationService -import org.matrix.android.sdk.api.session.crypto.verification.VerificationTransaction -import org.matrix.android.sdk.api.session.events.model.Event -import org.matrix.android.sdk.api.session.events.model.EventType -import org.matrix.android.sdk.api.session.events.model.RelationType -import org.matrix.android.sdk.api.session.events.model.content.EncryptedEventContent -import org.matrix.android.sdk.api.session.events.model.toModel -import org.matrix.android.sdk.api.session.room.model.message.MessageRelationContent -import org.matrix.android.sdk.api.session.room.model.message.MessageType -import org.matrix.android.sdk.api.session.room.model.message.MessageVerificationAcceptContent -import org.matrix.android.sdk.api.session.room.model.message.MessageVerificationCancelContent -import org.matrix.android.sdk.api.session.room.model.message.MessageVerificationDoneContent -import org.matrix.android.sdk.api.session.room.model.message.MessageVerificationKeyContent -import org.matrix.android.sdk.api.session.room.model.message.MessageVerificationMacContent -import org.matrix.android.sdk.api.session.room.model.message.MessageVerificationReadyContent -import org.matrix.android.sdk.api.session.room.model.message.MessageVerificationRequestContent -import org.matrix.android.sdk.api.session.room.model.message.MessageVerificationStartContent -import org.matrix.android.sdk.internal.crypto.DeviceListManager -import org.matrix.android.sdk.internal.crypto.actions.SetDeviceVerificationAction -import org.matrix.android.sdk.internal.crypto.model.rest.KeyVerificationAccept -import org.matrix.android.sdk.internal.crypto.model.rest.KeyVerificationCancel -import org.matrix.android.sdk.internal.crypto.model.rest.KeyVerificationDone -import org.matrix.android.sdk.internal.crypto.model.rest.KeyVerificationKey -import org.matrix.android.sdk.internal.crypto.model.rest.KeyVerificationMac -import org.matrix.android.sdk.internal.crypto.model.rest.KeyVerificationReady -import org.matrix.android.sdk.internal.crypto.model.rest.KeyVerificationRequest -import org.matrix.android.sdk.internal.crypto.model.rest.KeyVerificationStart -import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore -import org.matrix.android.sdk.internal.di.DeviceId -import org.matrix.android.sdk.internal.di.UserId -import org.matrix.android.sdk.internal.session.SessionScope -import timber.log.Timber -import javax.inject.Inject - -@SessionScope -internal class DefaultVerificationService @Inject constructor( - @UserId private val userId: String, - @DeviceId private val myDeviceId: String?, - private val cryptoStore: IMXCryptoStore, -// private val outgoingKeyRequestManager: OutgoingKeyRequestManager, -// private val secretShareManager: SecretShareManager, -// private val myDeviceInfoHolder: Lazy, - private val deviceListManager: DeviceListManager, - private val setDeviceVerificationAction: SetDeviceVerificationAction, - private val coroutineDispatchers: MatrixCoroutineDispatchers, -// private val verificationTransportRoomMessageFactor Oy: VerificationTransportRoomMessageFactory, -// private val verificationTransportToDeviceFactory: VerificationTransportToDeviceFactory, -// private val crossSigningService: CrossSigningService, - private val cryptoCoroutineScope: CoroutineScope, - verificationActorFactory: VerificationActor.Factory, -// private val taskExecutor: TaskExecutor, -// private val localEchoEventFactory: LocalEchoEventFactory, -// private val sendVerificationMessageTask: SendVerificationMessageTask, -// private val clock: Clock, -) : VerificationService { - - val executorScope = CoroutineScope(SupervisorJob() + coroutineDispatchers.dmVerif) - -// private val eventFlow: Flow - private val stateMachine: VerificationActor - - init { - stateMachine = verificationActorFactory.create(executorScope) - } - // It's obselete but not deprecated - // It's ok as it will be replaced by rust implementation -// lateinit var stateManagerActor : SendChannel -// val stateManagerActor = executorScope.actor { -// val actor = verificationActorFactory.create(channel) -// eventFlow = actor.eventFlow -// for (msg in channel) actor.onReceive(msg) -// } - -// private val mutex = Mutex() - - // Event received from the sync - fun onToDeviceEvent(event: Event) { - cryptoCoroutineScope.launch(coroutineDispatchers.dmVerif) { - when (event.getClearType()) { - EventType.KEY_VERIFICATION_START -> { - Timber.v("## SAS onToDeviceEvent ${event.getClearType()} from ${event.senderId?.take(10)}") - onStartRequestReceived(null, event) - } - EventType.KEY_VERIFICATION_CANCEL -> { - Timber.v("## SAS onToDeviceEvent ${event.getClearType()} from ${event.senderId?.take(10)}") - onCancelReceived(event) - } - EventType.KEY_VERIFICATION_ACCEPT -> { - Timber.v("## SAS onToDeviceEvent ${event.getClearType()} from ${event.senderId?.take(10)}") - onAcceptReceived(event) - } - EventType.KEY_VERIFICATION_KEY -> { - Timber.v("## SAS onToDeviceEvent ${event.getClearType()} from ${event.senderId?.take(10)}") - onKeyReceived(event) - } - EventType.KEY_VERIFICATION_MAC -> { - Timber.v("## SAS onToDeviceEvent ${event.getClearType()} from ${event.senderId?.take(10)}") - onMacReceived(event) - } - EventType.KEY_VERIFICATION_READY -> { - Timber.v("## SAS onToDeviceEvent ${event.getClearType()} from ${event.senderId?.take(10)}") - onReadyReceived(event) - } - EventType.KEY_VERIFICATION_DONE -> { - Timber.v("## SAS onToDeviceEvent ${event.getClearType()} from ${event.senderId?.take(10)}") - onDoneReceived(event) - } - MessageType.MSGTYPE_VERIFICATION_REQUEST -> { - Timber.v("## SAS onToDeviceEvent ${event.getClearType()} from ${event.senderId?.take(10)}") - onRequestReceived(event) - } - else -> { - // ignore - } - } - } - } - - fun onRoomEvent(roomId: String, event: Event) { - Timber.v("## SAS onRoomEvent ${event.getClearType()} from ${event.senderId?.take(10)}") - cryptoCoroutineScope.launch(coroutineDispatchers.dmVerif) { - when (event.getClearType()) { - EventType.KEY_VERIFICATION_START -> { - onRoomStartRequestReceived(roomId, event) - } - EventType.KEY_VERIFICATION_CANCEL -> { - // MultiSessions | ignore events if i didn't sent the start from this device, or accepted from this device - onRoomCancelReceived(roomId, event) - } - EventType.KEY_VERIFICATION_ACCEPT -> { - onRoomAcceptReceived(roomId, event) - } - EventType.KEY_VERIFICATION_KEY -> { - onRoomKeyRequestReceived(roomId, event) - } - EventType.KEY_VERIFICATION_MAC -> { - onRoomMacReceived(roomId, event) - } - EventType.KEY_VERIFICATION_READY -> { - onRoomReadyReceived(roomId, event) - } - EventType.KEY_VERIFICATION_DONE -> { - onRoomDoneReceived(roomId, event) - } -// EventType.MESSAGE -> { -// if (MessageType.MSGTYPE_VERIFICATION_REQUEST == event.getClearContent().toModel()?.msgType) { -// onRoomRequestReceived(roomId, event) -// } -// } - else -> { - // ignore - } - } - } - } - - override fun requestEventFlow(): Flow { - return stateMachine.eventFlow - } -// private var listeners = ArrayList() -// -// override fun addListener(listener: VerificationService.Listener) { -// if (!listeners.contains(listener)) { -// listeners.add(listener) -// } -// } -// -// override fun removeListener(listener: VerificationService.Listener) { -// listeners.remove(listener) -// } - -// private suspend fun dispatchTxAdded(tx: VerificationTransaction) { -// listeners.forEach { -// try { -// it.transactionCreated(tx) -// } catch (e: Throwable) { -// Timber.e(e, "## Error while notifying listeners") -// } -// } -// } -// -// private suspend fun dispatchTxUpdated(tx: VerificationTransaction) { -// listeners.forEach { -// try { -// it.transactionUpdated(tx) -// } catch (e: Throwable) { -// Timber.e(e, "## Error while notifying listeners for tx:${tx.state}") -// } -// } -// } -// -// private suspend fun dispatchRequestAdded(tx: PendingVerificationRequest) { -// Timber.v("## SAS dispatchRequestAdded txId:${tx.transactionId}") -// listeners.forEach { -// try { -// it.verificationRequestCreated(tx) -// } catch (e: Throwable) { -// Timber.e(e, "## Error while notifying listeners") -// } -// } -// } -// -// private suspend fun dispatchRequestUpdated(tx: PendingVerificationRequest) { -// listeners.forEach { -// try { -// it.verificationRequestUpdated(tx) -// } catch (e: Throwable) { -// Timber.e(e, "## Error while notifying listeners") -// } -// } -// } - - override suspend fun markedLocallyAsManuallyVerified(userId: String, deviceID: String) { - setDeviceVerificationAction.handle( - DeviceTrustLevel(crossSigningVerified = false, locallyVerified = true), - userId, - deviceID - ) - - // TODO -// listeners.forEach { -// try { -// it.markedAsManuallyVerified(userId, deviceID) -// } catch (e: Throwable) { -// Timber.e(e, "## Error while notifying listeners") -// } -// } - } - -// override suspend fun sasCodeMatch(theyMatch: Boolean, transactionId: String) { -// val deferred = CompletableDeferred() -// stateMachine.send( -// if (theyMatch) { -// VerificationIntent.ActionSASCodeMatches( -// transactionId, -// deferred, -// ) -// } else { -// VerificationIntent.ActionSASCodeDoesNotMatch( -// transactionId, -// deferred, -// ) -// } -// ) -// deferred.await() -// } - - suspend fun onRoomReadyFromOneOfMyOtherDevice(event: Event) { - val requestInfo = event.content.toModel() - ?: return - - stateMachine.send( - VerificationIntent.OnReadyByAnotherOfMySessionReceived( - transactionId = requestInfo.relatesTo?.eventId.orEmpty(), - fromUser = event.senderId.orEmpty(), - viaRoom = event.roomId - - ) - ) -// val requestId = requestInfo.relatesTo?.eventId ?: return -// getExistingVerificationRequestInRoom(event.roomId.orEmpty(), requestId)?.let { -// stateMachine.send( -// VerificationIntent.UpdateRequest( -// it.copy(handledByOtherSession = true) -// ) -// ) -// } - } - - private suspend fun onRequestReceived(event: Event) { - val validRequestInfo = event.getClearContent().toModel()?.asValidObject() - - if (validRequestInfo == null) { - // ignore - Timber.e("## SAS Received invalid key request") - return - } - val senderId = event.senderId ?: return - - val otherDeviceId = validRequestInfo.fromDevice - Timber.v("## SAS onRequestReceived from $senderId and device $otherDeviceId, txId:${validRequestInfo.transactionId}") - - val deferred = CompletableDeferred() - stateMachine.send( - VerificationIntent.OnVerificationRequestReceived( - senderId = senderId, - roomId = null, - timeStamp = event.originServerTs, - validRequestInfo = validRequestInfo, - ) - ) - deferred.await() - checkKeysAreDownloaded(senderId) - } - - suspend fun onRoomRequestReceived(roomId: String, event: Event) { - Timber.v("## SAS Verification request from ${event.senderId} in room ${event.roomId}") - val requestInfo = event.getClearContent().toModel() ?: return - val validRequestInfo = requestInfo - // copy the EventId to the transactionId - .copy(transactionId = event.eventId) - .asValidObject() ?: return - - val senderId = event.senderId ?: return - - if (requestInfo.toUserId != userId) { - // I should ignore this, it's not for me - Timber.w("## SAS Verification ignoring request from ${event.senderId}, not sent to me") - return - } - - stateMachine.send( - VerificationIntent.OnVerificationRequestReceived( - senderId = senderId, - roomId = roomId, - timeStamp = event.originServerTs, - validRequestInfo = validRequestInfo, - ) - ) - - // force download keys to ensure we are up to date - checkKeysAreDownloaded(senderId) -// // Remember this request -// val requestsForUser = pendingRequests.getOrPut(senderId) { mutableListOf() } -// -// val pendingVerificationRequest = PendingVerificationRequest( -// ageLocalTs = event.ageLocalTs ?: clock.epochMillis(), -// isIncoming = true, -// otherUserId = senderId, // requestInfo.toUserId, -// roomId = event.roomId, -// transactionId = event.eventId, -// localId = event.eventId!!, -// requestInfo = validRequestInfo -// ) -// requestsForUser.add(pendingVerificationRequest) -// dispatchRequestAdded(pendingVerificationRequest) - - /* - * After the m.key.verification.ready event is sent, either party can send an m.key.verification.start event - * to begin the verification. - * If both parties send an m.key.verification.start event, and they both specify the same verification method, - * then the event sent by the user whose user ID is the smallest is used, and the other m.key.verification.start - * event is ignored. - * In the case of a single user verifying two of their devices, the device ID is compared instead. - * If both parties send an m.key.verification.start event, but they specify different verification methods, - * the verification should be cancelled with a code of m.unexpected_message. - */ - } - - override suspend fun onPotentiallyInterestingEventRoomFailToDecrypt(event: Event) { - // When Should/Can we cancel?? - val relationContent = event.content.toModel()?.relatesTo - if (relationContent?.type == RelationType.REFERENCE) { - val relatedId = relationContent.eventId ?: return - val sender = event.senderId ?: return - val roomId = event.roomId ?: return - stateMachine.send( - VerificationIntent.OnUnableToDecryptVerificationEvent( - fromUser = sender, - roomId = roomId, - transactionId = relatedId - ) - ) -// // at least if request was sent by me, I can safely cancel without interfering -// pendingRequests[event.senderId]?.firstOrNull { -// it.transactionId == relatedId && !it.isIncoming -// }?.let { pr -> -// verificationTransportRoomMessageFactory.createTransport(event.roomId ?: "", null) -// .cancelTransaction( -// relatedId, -// event.senderId ?: "", -// event.getSenderKey() ?: "", -// CancelCode.InvalidMessage -// ) -// updatePendingRequest(pr.copy(cancelConclusion = CancelCode.InvalidMessage)) -// } - } - } - - private suspend fun onRoomStartRequestReceived(roomId: String, event: Event) { - val startReq = event.getClearContent().toModel() - ?.copy( - // relates_to is in clear in encrypted payload - relatesTo = event.content.toModel()?.relatesTo - ) - - val validStartReq = startReq?.asValidObject() ?: return - - stateMachine.send( - VerificationIntent.OnStartReceived( - fromUser = event.senderId.orEmpty(), - viaRoom = roomId, - validVerificationInfoStart = validStartReq, - ) - ) - } - - private suspend fun onStartRequestReceived(roomId: String? = null, event: Event) { - Timber.e("## SAS received Start request ${event.eventId}") - val startReq = event.getClearContent().toModel() - val validStartReq = startReq?.asValidObject() ?: return - Timber.v("## SAS received Start request $startReq") - - val otherUserId = event.senderId ?: return - stateMachine.send( - VerificationIntent.OnStartReceived( - fromUser = otherUserId, - viaRoom = roomId, - validVerificationInfoStart = validStartReq - ) - ) -// if (validStartReq == null) { -// Timber.e("## SAS received invalid verification request") -// if (startReq?.transactionId != null) { -// verificationTransportToDeviceFactory.createTransport(null).cancelTransaction( -// startReq.transactionId, -// otherUserId, -// startReq.fromDevice ?: event.getSenderKey()!!, -// CancelCode.UnknownMethod -// ) -// } -// return -// } -// // Download device keys prior to everything -// handleStart(otherUserId, validStartReq) { -// it.transport = verificationTransportToDeviceFactory.createTransport(it) -// }?.let { -// verificationTransportToDeviceFactory.createTransport(null).cancelTransaction( -// validStartReq.transactionId, -// otherUserId, -// validStartReq.fromDevice, -// it -// ) -// } - } - - /** - * Return a CancelCode to make the caller cancel the verification. Else return null - */ -// private suspend fun handleStart( -// otherUserId: String?, -// startReq: ValidVerificationInfoStart, -// txConfigure: (DefaultVerificationTransaction) -> Unit -// ): CancelCode? { -// Timber.d("## SAS onStartRequestReceived $startReq") -// otherUserId ?: return null // just ignore -// // if (otherUserId?.let { checkKeysAreDownloaded(it, startReq.fromDevice) } != null) { -// val tid = startReq.transactionId -// var existing = getExistingTransaction(otherUserId, tid) -// -// // After the m.key.verification.ready event is sent, either party can send an -// // m.key.verification.start event to begin the verification. If both parties -// // send an m.key.verification.start event, and they both specify the same -// // verification method, then the event sent by the user whose user ID is the -// // smallest is used, and the other m.key.verification.start event is ignored. -// // In the case of a single user verifying two of their devices, the device ID is -// // compared instead . -// if (existing is DefaultOutgoingSASDefaultVerificationTransaction) { -// val readyRequest = getExistingVerificationRequest(otherUserId, tid) -// if (readyRequest?.isReady == true) { -// if (isOtherPrioritary(otherUserId, existing.otherDeviceId ?: "")) { -// Timber.d("## SAS concurrent start isOtherPrioritary, clear") -// // The other is prioritary! -// // I should replace my outgoing with an incoming -// removeTransaction(otherUserId, tid) -// existing = null -// } else { -// Timber.d("## SAS concurrent start i am prioritary, ignore") -// // i am prioritary, ignore this start event! -// return null -// } -// } -// } -// -// when (startReq) { -// is ValidVerificationInfoStart.SasVerificationInfoStart -> { -// when (existing) { -// is SasVerificationTransaction -> { -// // should cancel both! -// Timber.v("## SAS onStartRequestReceived - Request exist with same id ${startReq.transactionId}") -// existing.cancel(CancelCode.UnexpectedMessage) -// // Already cancelled, so return null -// return null -// } -// is QrCodeVerificationTransaction -> { -// // Nothing to do? -// } -// null -> { -// getExistingTransactionsForUser(otherUserId) -// ?.filterIsInstance(SasVerificationTransaction::class.java) -// ?.takeIf { it.isNotEmpty() } -// ?.also { -// // Multiple keyshares between two devices: -// // any two devices may only have at most one key verification in flight at a time. -// Timber.v("## SAS onStartRequestReceived - Already a transaction with this user ${startReq.transactionId}") -// } -// ?.forEach { -// it.cancel(CancelCode.UnexpectedMessage) -// } -// ?.also { -// return CancelCode.UnexpectedMessage -// } -// } -// } -// -// // Ok we can create a SAS transaction -// Timber.v("## SAS onStartRequestReceived - request accepted ${startReq.transactionId}") -// // If there is a corresponding request, we can auto accept -// // as we are the one requesting in first place (or we accepted the request) -// // I need to check if the pending request was related to this device also -// val autoAccept = getExistingVerificationRequests(otherUserId).any { -// it.transactionId == startReq.transactionId && -// (it.requestInfo?.fromDevice == this.deviceId || it.readyInfo?.fromDevice == this.deviceId) -// } -// val tx = DefaultIncomingSASDefaultVerificationTransaction( -// // this, -// setDeviceVerificationAction, -// userId, -// deviceId, -// cryptoStore, -// crossSigningService, -// outgoingKeyRequestManager, -// secretShareManager, -// myDeviceInfoHolder.get().myDevice.fingerprint()!!, -// startReq.transactionId, -// otherUserId, -// autoAccept -// ).also { txConfigure(it) } -// addTransaction(tx) -// tx.onVerificationStart(startReq) -// return null -// } -// is ValidVerificationInfoStart.ReciprocateVerificationInfoStart -> { -// // Other user has scanned my QR code -// if (existing is DefaultQrCodeVerificationTransaction) { -// existing.onStartReceived(startReq) -// return null -// } else { -// Timber.w("## SAS onStartRequestReceived - unexpected message ${startReq.transactionId} / $existing") -// return CancelCode.UnexpectedMessage -// } -// } -// } -// // } else { -// // return CancelCode.UnexpectedMessage -// // } -// } - - private fun isOtherPrioritary(otherUserId: String, otherDeviceId: String): Boolean { - if (userId < otherUserId) { - return false - } else if (userId > otherUserId) { - return true - } else { - return otherDeviceId < myDeviceId ?: "" - } - } - - private suspend fun checkKeysAreDownloaded( - otherUserId: String, - ): Boolean { - return try { - deviceListManager.downloadKeys(listOf(otherUserId), false) - .getUserDeviceIds(otherUserId) - ?.contains(userId) - ?: deviceListManager.downloadKeys(listOf(otherUserId), true) - .getUserDeviceIds(otherUserId) - ?.contains(userId) - ?: false - } catch (e: Exception) { - false - } - } - - private suspend fun onRoomCancelReceived(roomId: String, event: Event) { - val cancelReq = event.getClearContent().toModel() - ?.copy( - // relates_to is in clear in encrypted payload - relatesTo = event.content.toModel()?.relatesTo - ) - - val validCancelReq = cancelReq?.asValidObject() ?: return - event.senderId ?: return - stateMachine.send( - VerificationIntent.OnCancelReceived( - viaRoom = roomId, - fromUser = event.senderId, - validCancel = validCancelReq - ) - ) - -// if (validCancelReq == null) { -// // ignore -// Timber.e("## SAS Received invalid cancel request") -// // TODO should we cancel? -// return -// } -// getExistingVerificationRequest(event.senderId ?: "", validCancelReq.transactionId)?.let { -// updatePendingRequest(it.copy(cancelConclusion = safeValueOf(validCancelReq.code))) -// } -// handleOnCancel(event.senderId!!, validCancelReq) - } - - private suspend fun onCancelReceived(event: Event) { - Timber.v("## SAS onCancelReceived") - val cancelReq = event.getClearContent().toModel()?.asValidObject() - ?: return - - event.senderId ?: return - stateMachine.send( - VerificationIntent.OnCancelReceived( - viaRoom = null, - fromUser = event.senderId, - validCancel = cancelReq - ) - ) - } - -// private fun handleOnCancel(otherUserId: String, cancelReq: ValidVerificationInfoCancel) { -// Timber.v("## SAS onCancelReceived otherUser: $otherUserId reason: ${cancelReq.reason}") -// -// val existingTransaction = getExistingTransaction(otherUserId, cancelReq.transactionId) -// val existingRequest = getExistingVerificationRequest(otherUserId, cancelReq.transactionId) -// -// if (existingRequest != null) { -// // Mark this request as cancelled -// updatePendingRequest( -// existingRequest.copy( -// cancelConclusion = safeValueOf(cancelReq.code) -// ) -// ) -// } -// -// existingTransaction?.state = VerificationTxState.Cancelled(safeValueOf(cancelReq.code), false) -// } - - private suspend fun onRoomAcceptReceived(roomId: String, event: Event) { - Timber.d("## SAS Received Accept via DM $event") - val accept = event.getClearContent().toModel() - ?.copy( - // relates_to is in clear in encrypted payload - relatesTo = event.content.toModel()?.relatesTo - ) - ?: return - - val validAccept = accept.asValidObject() ?: return - - handleAccept(roomId, validAccept, event.senderId!!) - } - - private suspend fun onAcceptReceived(event: Event) { - Timber.d("## SAS Received Accept $event") - val acceptReq = event.getClearContent().toModel()?.asValidObject() ?: return - handleAccept(null, acceptReq, event.senderId!!) - } - - private suspend fun handleAccept(roomId: String?, acceptReq: ValidVerificationInfoAccept, senderId: String) { - stateMachine.send( - VerificationIntent.OnAcceptReceived( - viaRoom = roomId, - validAccept = acceptReq, - fromUser = senderId - ) - ) - } - - private suspend fun onRoomKeyRequestReceived(roomId: String, event: Event) { - val keyReq = event.getClearContent().toModel() - ?.copy( - // relates_to is in clear in encrypted payload - relatesTo = event.content.toModel()?.relatesTo - ) - ?.asValidObject() - if (keyReq == null) { - // ignore - Timber.e("## SAS Received invalid key request") - // TODO should we cancel? - return - } - handleKeyReceived(roomId, event, keyReq) - } - - private suspend fun onKeyReceived(event: Event) { - val keyReq = event.getClearContent().toModel()?.asValidObject() - - if (keyReq == null) { - // ignore - Timber.e("## SAS Received invalid key request") - return - } - handleKeyReceived(null, event, keyReq) - } - - private suspend fun handleKeyReceived(roomId: String?, event: Event, keyReq: ValidVerificationInfoKey) { - Timber.d("## SAS Received Key from ${event.senderId} with info $keyReq") - val otherUserId = event.senderId ?: return - stateMachine.send( - VerificationIntent.OnKeyReceived( - roomId, - otherUserId, - keyReq - ) - ) - } - - private suspend fun onRoomMacReceived(roomId: String, event: Event) { - val macReq = event.getClearContent().toModel() - ?.copy( - // relates_to is in clear in encrypted payload - relatesTo = event.content.toModel()?.relatesTo - ) - ?.asValidObject() - if (macReq == null || event.senderId == null) { - // ignore - Timber.e("## SAS Received invalid mac request") - // TODO should we cancel? - return - } - stateMachine.send( - VerificationIntent.OnMacReceived( - viaRoom = roomId, - fromUser = event.senderId, - validMac = macReq - ) - ) - } - - private suspend fun onRoomReadyReceived(roomId: String, event: Event) { - val readyReq = event.getClearContent().toModel() - ?.copy( - // relates_to is in clear in encrypted payload - relatesTo = event.content.toModel()?.relatesTo - ) - ?.asValidObject() - - if (readyReq == null || event.senderId == null) { - // ignore - Timber.e("## SAS Received invalid room ready request $readyReq senderId=${event.senderId}") - Timber.e("## SAS Received invalid room ready content=${event.getClearContent()}") - Timber.e("## SAS Received invalid room ready content=${event}") - // TODO should we cancel? - return - } - stateMachine.send( - VerificationIntent.OnReadyReceived( - transactionId = readyReq.transactionId, - fromUser = event.senderId, - viaRoom = roomId, - readyInfo = readyReq - ) - ) - // if it's a ready send by one of my other device I should stop handling in it on my side. -// if (event.senderId == userId && readyReq.fromDevice != deviceId) { -// getExistingVerificationRequestInRoom(roomId, readyReq.transactionId)?.let { -// updatePendingRequest( -// it.copy( -// handledByOtherSession = true -// ) -// ) -// } -// return -// } -// -// handleReadyReceived(event.senderId, readyReq) { -// verificationTransportRoomMessageFactory.createTransport(roomId, it) -// } - } - - private suspend fun onReadyReceived(event: Event) { - val readyReq = event.getClearContent().toModel()?.asValidObject() - Timber.v("## SAS onReadyReceived $readyReq") - - if (readyReq == null || event.senderId == null) { - // ignore - Timber.e("## SAS Received invalid ready request $readyReq senderId=${event.senderId}") - Timber.e("## SAS Received invalid ready content=${event.getClearContent()}") - // TODO should we cancel? - return - } - - stateMachine.send( - VerificationIntent.OnReadyReceived( - transactionId = readyReq.transactionId, - fromUser = event.senderId, - viaRoom = null, - readyInfo = readyReq - ) - ) -// if (checkKeysAreDownloaded(event.senderId, readyReq.fromDevice) == null) { -// Timber.e("## SAS Verification device ${readyReq.fromDevice} is not known") -// // TODO cancel? -// return -// } -// -// handleReadyReceived(event.senderId, readyReq) { -// verificationTransportToDeviceFactory.createTransport(it) -// } - } - - private suspend fun onDoneReceived(event: Event) { - Timber.v("## onDoneReceived") - val doneReq = event.getClearContent().toModel()?.asValidObject() - if (doneReq == null || event.senderId == null) { - // ignore - Timber.e("## SAS Received invalid done request ${doneReq}") - return - } - stateMachine.send( - VerificationIntent.OnDoneReceived( - transactionId = doneReq.transactionId, - fromUser = event.senderId, - viaRoom = null, - ) - ) - -// handleDoneReceived(event.senderId, doneReq) -// -// if (event.senderId == userId) { -// // We only send gossiping request when the other sent us a done -// // We can ask without checking too much thinks (like trust), because we will check validity of secret on reception -// getExistingTransaction(userId, doneReq.transactionId) -// ?: getOldTransaction(userId, doneReq.transactionId) -// ?.let { vt -> -// val otherDeviceId = vt.otherDeviceId ?: return@let -// if (!crossSigningService.canCrossSign()) { -// cryptoCoroutineScope.launch { -// secretShareManager.requestSecretTo(otherDeviceId, MASTER_KEY_SSSS_NAME) -// secretShareManager.requestSecretTo(otherDeviceId, SELF_SIGNING_KEY_SSSS_NAME) -// secretShareManager.requestSecretTo(otherDeviceId, USER_SIGNING_KEY_SSSS_NAME) -// secretShareManager.requestSecretTo(otherDeviceId, KEYBACKUP_SECRET_SSSS_NAME) -// } -// } -// } -// } - } - -// private suspend fun handleDoneReceived(senderId: String, doneReq: ValidVerificationDone) { -// Timber.v("## SAS Done received $doneReq") -// val existing = getExistingTransaction(senderId, doneReq.transactionId) -// if (existing == null) { -// Timber.e("## SAS Received Invalid done unknown request:${doneReq.transactionId} ") -// return -// } -// if (existing is DefaultQrCodeVerificationTransaction) { -// existing.onDoneReceived() -// } else { -// // SAS do not care for now? -// } -// -// // Now transactions are updated, let's also update Requests -// val existingRequest = getExistingVerificationRequests(senderId).find { it.transactionId == doneReq.transactionId } -// if (existingRequest == null) { -// Timber.e("## SAS Received Done for unknown request txId:${doneReq.transactionId}") -// return -// } -// updatePendingRequest(existingRequest.copy(isSuccessful = true)) -// } - - private suspend fun onRoomDoneReceived(roomId: String, event: Event) { - val doneReq = event.getClearContent().toModel() - ?.copy( - // relates_to is in clear in encrypted payload - relatesTo = event.content.toModel()?.relatesTo - ) - ?.asValidObject() - - if (doneReq == null || event.senderId == null) { - // ignore - Timber.e("## SAS Received invalid Done request ${doneReq}") - // TODO should we cancel? - return - } - - stateMachine.send( - VerificationIntent.OnDoneReceived( - transactionId = doneReq.transactionId, - fromUser = event.senderId, - viaRoom = roomId, - ) - ) - } - - private suspend fun onMacReceived(event: Event) { - val macReq = event.getClearContent().toModel()?.asValidObject() - - if (macReq == null || event.senderId == null) { - // ignore - Timber.e("## SAS Received invalid mac request") - return - } - stateMachine.send( - VerificationIntent.OnMacReceived( - viaRoom = null, - fromUser = event.senderId, - validMac = macReq - ) - ) - } -// -// private suspend fun handleMacReceived(senderId: String, macReq: ValidVerificationInfoMac) { -// Timber.v("## SAS Received $macReq") -// val existing = getExistingTransaction(senderId, macReq.transactionId) -// if (existing == null) { -// Timber.e("## SAS Received Mac for unknown transaction ${macReq.transactionId}") -// return -// } -// if (existing is SASDefaultVerificationTransaction) { -// existing.onKeyVerificationMac(macReq) -// } else { -// // not other types known for now -// } -// } - -// private suspend fun handleReadyReceived( -// senderId: String, -// readyReq: ValidVerificationInfoReady, -// transportCreator: (DefaultVerificationTransaction) -> VerificationTransport -// ) { -// val existingRequest = getExistingVerificationRequests(senderId).find { it.transactionId == readyReq.transactionId } -// if (existingRequest == null) { -// Timber.e("## SAS Received Ready for unknown request txId:${readyReq.transactionId} fromDevice ${readyReq.fromDevice}") -// return -// } -// -// val qrCodeData = readyReq.methods -// // Check if other user is able to scan QR code -// .takeIf { it.contains(VERIFICATION_METHOD_QR_CODE_SCAN) } -// ?.let { -// createQrCodeData(existingRequest.transactionId, existingRequest.otherUserId, readyReq.fromDevice) -// } -// -// if (readyReq.methods.contains(VERIFICATION_METHOD_RECIPROCATE)) { -// // Create the pending transaction -// val tx = DefaultQrCodeVerificationTransaction( -// setDeviceVerificationAction = setDeviceVerificationAction, -// transactionId = readyReq.transactionId, -// otherUserId = senderId, -// otherDeviceId = readyReq.fromDevice, -// crossSigningService = crossSigningService, -// outgoingKeyRequestManager = outgoingKeyRequestManager, -// secretShareManager = secretShareManager, -// cryptoStore = cryptoStore, -// qrCodeData = qrCodeData, -// userId = userId, -// deviceId = deviceId ?: "", -// isIncoming = false -// ) -// -// tx.transport = transportCreator.invoke(tx) -// -// addTransaction(tx) -// } -// -// updatePendingRequest( -// existingRequest.copy( -// readyInfo = readyReq -// ) -// ) -// -// // if it's a to_device verification request, we need to notify others that the -// // request was accepted by this one -// if (existingRequest.roomId == null) { -// notifyOthersOfAcceptance(existingRequest, readyReq.fromDevice) -// } -// } - - /** - * Gets a list of device ids excluding the current one. - */ -// private fun getMyOtherDeviceIds(): List = cryptoStore.getUserDevices(userId)?.keys?.filter { it != deviceId }.orEmpty() - - /** - * Notifies other devices that the current verification request is being handled by [acceptedByDeviceId]. - */ -// private fun notifyOthersOfAcceptance(request: PendingVerificationRequest, acceptedByDeviceId: String) { -// val otherUserId = request.otherUserId -// // this user should be me, as we use to device verification only for self verification -// // but the spec is not that restrictive -// val deviceIds = cryptoStore.getUserDevices(otherUserId)?.keys -// ?.filter { it != acceptedByDeviceId } -// // if it's me we don't want to send self cancel -// ?.filter { it != deviceId } -// .orEmpty() -// -// val transport = verificationTransportToDeviceFactory.createTransport(null) -// transport.cancelTransaction( -// request.transactionId.orEmpty(), -// otherUserId, -// deviceIds, -// CancelCode.AcceptedByAnotherDevice -// ) -// } - -// private suspend fun createQrCodeData(requestId: String, otherUserId: String, otherDeviceId: String?): QrCodeData? { -// // requestId ?: run { -// // Timber.w("## Unknown requestId") -// // return null -// // } -// -// return when { -// userId != otherUserId -> -// createQrCodeDataForDistinctUser(requestId, otherUserId) -// crossSigningService.isCrossSigningVerified() -> -// // This is a self verification and I am the old device (Osborne2) -// createQrCodeDataForVerifiedDevice(requestId, otherDeviceId) -// else -> -// // This is a self verification and I am the new device (Dynabook) -// createQrCodeDataForUnVerifiedDevice(requestId) -// } -// } - -// private suspend fun createQrCodeDataForDistinctUser(requestId: String, otherUserId: String): QrCodeData.VerifyingAnotherUser? { -// val myMasterKey = crossSigningService.getMyCrossSigningKeys() -// ?.masterKey() -// ?.unpaddedBase64PublicKey -// ?: run { -// Timber.w("## Unable to get my master key") -// return null -// } -// -// val otherUserMasterKey = crossSigningService.getUserCrossSigningKeys(otherUserId) -// ?.masterKey() -// ?.unpaddedBase64PublicKey -// ?: run { -// Timber.w("## Unable to get other user master key") -// return null -// } -// -// return QrCodeData.VerifyingAnotherUser( -// transactionId = requestId, -// userMasterCrossSigningPublicKey = myMasterKey, -// otherUserMasterCrossSigningPublicKey = otherUserMasterKey, -// sharedSecret = generateSharedSecretV2() -// ) -// } - - // Create a QR code to display on the old device (Osborne2) -// private suspend fun createQrCodeDataForVerifiedDevice(requestId: String, otherDeviceId: String?): QrCodeData.SelfVerifyingMasterKeyTrusted? { -// val myMasterKey = crossSigningService.getMyCrossSigningKeys() -// ?.masterKey() -// ?.unpaddedBase64PublicKey -// ?: run { -// Timber.w("## Unable to get my master key") -// return null -// } -// -// val otherDeviceKey = otherDeviceId -// ?.let { -// cryptoStore.getUserDevice(userId, otherDeviceId)?.fingerprint() -// } -// ?: run { -// Timber.w("## Unable to get other device data") -// return null -// } -// -// return QrCodeData.SelfVerifyingMasterKeyTrusted( -// transactionId = requestId, -// userMasterCrossSigningPublicKey = myMasterKey, -// otherDeviceKey = otherDeviceKey, -// sharedSecret = generateSharedSecretV2() -// ) -// } - - // Create a QR code to display on the new device (Dynabook) -// private suspend fun createQrCodeDataForUnVerifiedDevice(requestId: String): QrCodeData.SelfVerifyingMasterKeyNotTrusted? { -// val myMasterKey = crossSigningService.getMyCrossSigningKeys() -// ?.masterKey() -// ?.unpaddedBase64PublicKey -// ?: run { -// Timber.w("## Unable to get my master key") -// return null -// } -// -// val myDeviceKey = myDeviceInfoHolder.get().myDevice.fingerprint() -// ?: run { -// Timber.w("## Unable to get my fingerprint") -// return null -// } -// -// return QrCodeData.SelfVerifyingMasterKeyNotTrusted( -// transactionId = requestId, -// deviceKey = myDeviceKey, -// userMasterCrossSigningPublicKey = myMasterKey, -// sharedSecret = generateSharedSecretV2() -// ) -// } - -// private fun handleDoneReceived(senderId: String, doneInfo: ValidVerificationDone) { -// val existingRequest = getExistingVerificationRequest(senderId)?.find { it.transactionId == doneInfo.transactionId } -// if (existingRequest == null) { -// Timber.e("## SAS Received Done for unknown request txId:${doneInfo.transactionId}") -// return -// } -// updatePendingRequest(existingRequest.copy(isSuccessful = true)) -// } - - // TODO All this methods should be delegated to a TransactionStore - override suspend fun getExistingTransaction(otherUserId: String, tid: String): VerificationTransaction? { - val deferred = CompletableDeferred() - stateMachine.send( - VerificationIntent.GetExistingTransaction( - fromUser = otherUserId, - transactionId = tid, - deferred = deferred - ) - ) - return deferred.await() - } - - override suspend fun getExistingVerificationRequests(otherUserId: String): List { - val deferred = CompletableDeferred>() - stateMachine.send( - VerificationIntent.GetExistingRequestsForUser( - userId = otherUserId, - deferred = deferred - ) - ) - return deferred.await() - } - - override suspend fun getExistingVerificationRequest(otherUserId: String, tid: String?): PendingVerificationRequest? { - val deferred = CompletableDeferred() - tid ?: return null - stateMachine.send( - VerificationIntent.GetExistingRequest( - transactionId = tid, - otherUserId = otherUserId, - deferred = deferred - ) - ) - return deferred.await() - } - - override suspend fun getExistingVerificationRequestInRoom(roomId: String, tid: String): PendingVerificationRequest? { - val deferred = CompletableDeferred() - stateMachine.send( - VerificationIntent.GetExistingRequestInRoom( - transactionId = tid, roomId = roomId, - deferred = deferred - ) - ) - return deferred.await() - } - -// private suspend fun getExistingTransactionsForUser(otherUser: String): Collection? { -// mutex.withLock { -// return txMap[otherUser]?.values -// } -// } - -// private suspend fun removeTransaction(otherUser: String, tid: String) { -// mutex.withLock { -// txMap[otherUser]?.remove(tid)?.also { -// it.removeListener(this) -// } -// }?.let { -// rememberOldTransaction(it) -// } -// } - -// private suspend fun addTransaction(tx: DefaultVerificationTransaction) { -// mutex.withLock { -// val txInnerMap = txMap.getOrPut(tx.otherUserId) { HashMap() } -// txInnerMap[tx.transactionId] = tx -// dispatchTxAdded(tx) -// tx.addListener(this) -// } -// } - -// private suspend fun rememberOldTransaction(tx: DefaultVerificationTransaction) { -// mutex.withLock { -// pastTransactions.getOrPut(tx.otherUserId) { HashMap() }[tx.transactionId] = tx -// } -// } - -// private suspend fun getOldTransaction(userId: String, tid: String?): DefaultVerificationTransaction? { -// return tid?.let { -// mutex.withLock { -// pastTransactions[userId]?.get(it) -// } -// } -// } - - override suspend fun startKeyVerification(method: VerificationMethod, otherUserId: String, requestId: String): String? { - require(method == VerificationMethod.SAS) { "Unknown verification method" } - val deferred = CompletableDeferred() - stateMachine.send( - VerificationIntent.ActionStartSasVerification( - otherUserId = otherUserId, - requestId = requestId, - deferred = deferred - ) - ) - return deferred.await().transactionId - } - - override suspend fun reciprocateQRVerification(otherUserId: String, requestId: String, scannedData: String): String? { - val deferred = CompletableDeferred() - stateMachine.send( - VerificationIntent.ActionReciprocateQrVerification( - otherUserId = otherUserId, - requestId = requestId, - scannedData = scannedData, - deferred = deferred - ) - ) - return deferred.await()?.transactionId - } - - override suspend fun requestKeyVerificationInDMs( - methods: List, - otherUserId: String, - roomId: String, - localId: String? - ): PendingVerificationRequest { - Timber.i("## SAS Requesting verification to user: $otherUserId in room $roomId") - - checkKeysAreDownloaded(otherUserId) - -// val requestsForUser = pendingRequests.getOrPut(otherUserId) { mutableListOf() } - -// val transport = verificationTransportRoomMessageFactory.createTransport(roomId) - - val deferred = CompletableDeferred() - stateMachine.send( - VerificationIntent.ActionRequestVerification( - roomId = roomId, - otherUserId = otherUserId, - methods = methods, - deferred = deferred - ) - ) - - return deferred.await() -// result.toCancel.forEach { -// try { -// transport.cancelTransaction(it.transactionId.orEmpty(), it.otherUserId, "", CancelCode.User) -// } catch (failure: Throwable) { -// // continue anyhow -// } -// } -// val verificationRequest = result.request -// -// val requestInfo = verificationRequest.requestInfo -// try { -// val sentRequest = transport.sendVerificationRequest(requestInfo.methods, verificationRequest.localId, otherUserId, roomId, null) -// // We need to update with the syncedID -// val updatedRequest = verificationRequest.copy( -// transactionId = sentRequest.transactionId, -// // localId stays different -// requestInfo = sentRequest -// ) -// updatePendingRequest(updatedRequest) -// return updatedRequest -// } catch (failure: Throwable) { -// Timber.i("## Failed to send request $verificationRequest") -// stateManagerActor.send( -// VerificationIntent.FailToSendRequest(verificationRequest) -// ) -// throw failure -// } - } - - override suspend fun requestSelfKeyVerification(methods: List): PendingVerificationRequest { - return requestDeviceVerification(methods, userId, null) - } - - override suspend fun requestDeviceVerification(methods: List, otherUserId: String, otherDeviceId: String?): PendingVerificationRequest { - // TODO refactor this with the DM one - - val targetDevices = otherDeviceId?.let { listOf(it) } - ?: cryptoStore.getUserDevices(otherUserId) - ?.filter { it.key != myDeviceId } - ?.values?.map { it.deviceId }.orEmpty() - - Timber.i("## Requesting verification to user: $otherUserId with device list $targetDevices") - -// val transport = verificationTransportToDeviceFactory.createTransport(otherUserId, otherDeviceId) - - val deferred = CompletableDeferred() - stateMachine.send( - VerificationIntent.ActionRequestVerification( - roomId = null, - otherUserId = otherUserId, - targetDevices = targetDevices, - methods = methods, - deferred = deferred - ) - ) - - return deferred.await() -// result.toCancel.forEach { -// try { -// transport.cancelTransaction(it.transactionId.orEmpty(), it.otherUserId, "", CancelCode.User) -// } catch (failure: Throwable) { -// // continue anyhow -// } -// } -// val verificationRequest = result.request -// -// val requestInfo = verificationRequest.requestInfo -// try { -// val sentRequest = transport.sendVerificationRequest(requestInfo.methods, verificationRequest.localId, otherUserId, null, targetDevices) -// // We need to update with the syncedID -// val updatedRequest = verificationRequest.copy( -// transactionId = sentRequest.transactionId, -// // localId stays different -// requestInfo = sentRequest -// ) -// updatePendingRequest(updatedRequest) -// return updatedRequest -// } catch (failure: Throwable) { -// Timber.i("## Failed to send request $verificationRequest") -// stateManagerActor.send( -// VerificationIntent.FailToSendRequest(verificationRequest) -// ) -// throw failure -// } - -// // Cancel existing pending requests? -// requestsForUser.toList().forEach { existingRequest -> -// existingRequest.transactionId?.let { tid -> -// if (!existingRequest.isFinished) { -// Timber.d("## SAS, cancelling pending requests to start a new one") -// updatePendingRequest(existingRequest.copy(cancelConclusion = CancelCode.User)) -// existingRequest.targetDevices?.forEach { -// transport.cancelTransaction(tid, existingRequest.otherUserId, it, CancelCode.User) -// } -// } -// } -// } -// -// val localId = LocalEcho.createLocalEchoId() -// -// val verificationRequest = PendingVerificationRequest( -// transactionId = localId, -// ageLocalTs = clock.epochMillis(), -// isIncoming = false, -// roomId = null, -// localId = localId, -// otherUserId = otherUserId, -// targetDevices = targetDevices -// ) -// -// // We can SCAN or SHOW QR codes only if cross-signing is enabled -// val methodValues = if (crossSigningService.isCrossSigningInitialized()) { -// // Add reciprocate method if application declares it can scan or show QR codes -// // Not sure if it ok to do that (?) -// val reciprocateMethod = methods -// .firstOrNull { it == VerificationMethod.QR_CODE_SCAN || it == VerificationMethod.QR_CODE_SHOW } -// ?.let { listOf(VERIFICATION_METHOD_RECIPROCATE) }.orEmpty() -// methods.map { it.toValue() } + reciprocateMethod -// } else { -// // Filter out SCAN and SHOW qr code method -// methods -// .filter { it != VerificationMethod.QR_CODE_SHOW && it != VerificationMethod.QR_CODE_SCAN } -// .map { it.toValue() } -// } -// .distinct() -// -// dispatchRequestAdded(verificationRequest) -// val info = transport.sendVerificationRequest(methodValues, localId, otherUserId, null, targetDevices) -// // Nothing special to do in to device mode -// updatePendingRequest( -// verificationRequest.copy( -// // localId stays different -// requestInfo = info -// ) -// ) -// -// requestsForUser.add(verificationRequest) -// -// return verificationRequest - } - - override suspend fun cancelVerificationRequest(request: PendingVerificationRequest) { - val deferred = CompletableDeferred() - stateMachine.send( - VerificationIntent.ActionCancel( - transactionId = request.transactionId, - deferred - ) - ) - deferred.await() -// if (request.roomId != null) { -// val transport = verificationTransportRoomMessageFactory.createTransport(request.roomId) -// transport.cancelTransaction(request.transactionId ?: "", request.otherUserId, null, CancelCode.User) -// } else { -// // TODO is there a difference between incoming/outgoing? -// val transport = verificationTransportToDeviceFactory.createTransport(request.otherUserId, null) -// request.targetDevices?.forEach { deviceId -> -// transport.cancelTransaction(request.transactionId ?: "", request.otherUserId, deviceId, CancelCode.User) -// } -// } - } - - override suspend fun cancelVerificationRequest(otherUserId: String, transactionId: String) { - getExistingVerificationRequest(otherUserId, transactionId)?.let { - cancelVerificationRequest(it) - } - } - - override suspend fun declineVerificationRequestInDMs(otherUserId: String, transactionId: String, roomId: String) { - val deferred = CompletableDeferred() - stateMachine.send( - VerificationIntent.ActionCancel( - transactionId, - deferred - ) - ) - deferred.await() -// verificationTransportRoomMessageFactory.createTransport(roomId, null) -// .cancelTransaction(transactionId, otherUserId, null, CancelCode.User) -// -// getExistingVerificationRequest(otherUserId, transactionId)?.let { -// updatePendingRequest( -// it.copy( -// cancelConclusion = CancelCode.User -// ) -// ) -// } - } - -// private suspend fun updatePendingRequest(updated: PendingVerificationRequest) { -// stateManagerActor.send( -// VerificationIntent.UpdateRequest(updated) -// ) -// } - -// override fun beginKeyVerificationInDMs( -// method: VerificationMethod, -// transactionId: String, -// roomId: String, -// otherUserId: String, -// otherDeviceId: String -// ): String { -// if (method == VerificationMethod.SAS) { -// val tx = DefaultOutgoingSASDefaultVerificationTransaction( -// setDeviceVerificationAction, -// userId, -// deviceId, -// cryptoStore, -// crossSigningService, -// outgoingKeyRequestManager, -// secretShareManager, -// myDeviceInfoHolder.get().myDevice.fingerprint()!!, -// transactionId, -// otherUserId, -// otherDeviceId -// ) -// tx.transport = verificationTransportRoomMessageFactory.createTransport(roomId, tx) -// addTransaction(tx) -// -// tx.start() -// return transactionId -// } else { -// throw IllegalArgumentException("Unknown verification method") -// } -// } - -// override fun readyPendingVerificationInDMs( -// methods: List, -// otherUserId: String, -// roomId: String, -// transactionId: String -// ): Boolean { -// Timber.v("## SAS readyPendingVerificationInDMs $otherUserId room:$roomId tx:$transactionId") -// // Let's find the related request -// val existingRequest = getExistingVerificationRequest(otherUserId, transactionId) -// if (existingRequest != null) { -// // we need to send a ready event, with matching methods -// val transport = verificationTransportRoomMessageFactory.createTransport(roomId, null) -// val computedMethods = computeReadyMethods( -// transactionId, -// otherUserId, -// existingRequest.requestInfo?.fromDevice ?: "", -// existingRequest.requestInfo?.methods, -// methods -// ) { -// verificationTransportRoomMessageFactory.createTransport(roomId, it) -// } -// if (methods.isNullOrEmpty()) { -// Timber.i("Cannot ready this request, no common methods found txId:$transactionId") -// // TODO buttons should not be shown in this case? -// return false -// } -// // TODO this is not yet related to a transaction, maybe we should use another method like for cancel? -// val readyMsg = transport.createReady(transactionId, deviceId ?: "", computedMethods) -// transport.sendToOther( -// EventType.KEY_VERIFICATION_READY, -// readyMsg, -// VerificationTxState.None, -// CancelCode.User, -// null // TODO handle error? -// ) -// updatePendingRequest(existingRequest.copy(readyInfo = readyMsg.asValidObject())) -// return true -// } else { -// Timber.e("## SAS readyPendingVerificationInDMs Verification not found") -// // :/ should not be possible... unless live observer very slow -// return false -// } -// } - - override suspend fun readyPendingVerification( - methods: List, - otherUserId: String, - transactionId: String - ): Boolean { - Timber.v("## SAS readyPendingVerification $otherUserId tx:$transactionId") - val deferred = CompletableDeferred() - stateMachine.send( - VerificationIntent.ActionReadyRequest( - transactionId = transactionId, - methods = methods, - deferred = deferred - ) - ) -// val request = deferred.await() -// if (request?.readyInfo != null) { -// val transport = transportForRequest(request) -// try { -// val readyMsg = transport.createReady(transactionId, request.readyInfo.fromDevice, request.readyInfo.methods) -// transport.sendVerificationReady( -// readyMsg, -// request.otherUserId, -// request.requestInfo?.fromDevice, -// request.roomId -// ) -// return true -// } catch (failure: Throwable) { -// // revert back -// stateManagerActor.send( -// VerificationIntent.UpdateRequest( -// request.copy( -// readyInfo = null -// ) -// ) -// ) -// } -// } - return deferred.await() != null - -// // Let's find the related request -// val existingRequest = getExistingVerificationRequest(otherUserId, transactionId) -// ?: return false.also { -// Timber.e("## SAS readyPendingVerification Verification not found") -// // :/ should not be possible... unless live observer very slow -// } -// // we need to send a ready event, with matching methods -// -// val otherUserMethods = existingRequest.requestInfo?.methods.orEmpty() -// val computedMethods = computeReadyMethods( -// // transactionId, -// // otherUserId, -// // existingRequest.requestInfo?.fromDevice ?: "", -// otherUserMethods, -// methods -// ) -// -// if (methods.isEmpty()) { -// Timber.i("## SAS Cannot ready this request, no common methods found txId:$transactionId") -// // TODO buttons should not be shown in this case? -// return false -// } -// // TODO this is not yet related to a transaction, maybe we should use another method like for cancel? -// val transport = if (existingRequest.roomId != null) { -// verificationTransportRoomMessageFactory.createTransport(existingRequest.roomId) -// } else { -// verificationTransportToDeviceFactory.createTransport() -// } -// val readyMsg = transport.createReady(transactionId, deviceId ?: "", computedMethods).also { -// Timber.i("## SAS created ready Message ${it}") -// } -// -// val qrCodeData = if (otherUserMethods.canScanCode() && methods.contains(VerificationMethod.QR_CODE_SHOW)) { -// createQrCodeData(transactionId, otherUserId, existingRequest.requestInfo?.fromDevice) -// } else { -// null -// } -// -// transport.sendVerificationReady(readyMsg, existingRequest.otherUserId, existingRequest.requestInfo?.fromDevice, existingRequest.roomId) -// updatePendingRequest( -// existingRequest.copy( -// readyInfo = readyMsg.asValidObject(), -// qrCodeText = qrCodeData?.toEncodedString() -// ) -// ) -// return true - } - -// private fun transportForRequest(request: PendingVerificationRequest): VerificationTransport { -// return if (request.roomId != null) { -// verificationTransportRoomMessageFactory.createTransport(request.roomId) -// } else { -// verificationTransportToDeviceFactory.createTransport( -// request.otherUserId, -// request.requestInfo?.fromDevice.orEmpty() -// ) -// } -// } - -// private suspend fun computeReadyMethods( -// // transactionId: String, -// // otherUserId: String, -// // otherDeviceId: String, -// otherUserMethods: List?, -// methods: List, -// transportCreator: (DefaultVerificationTransaction) -> VerificationTransport -// ): List { -// if (otherUserMethods.isNullOrEmpty()) { -// return emptyList() -// } -// -// val result = mutableSetOf() -// -// if (VERIFICATION_METHOD_SAS in otherUserMethods && VerificationMethod.SAS in methods) { -// // Other can do SAS and so do I -// result.add(VERIFICATION_METHOD_SAS) -// } -// -// if (VERIFICATION_METHOD_QR_CODE_SCAN in otherUserMethods || VERIFICATION_METHOD_QR_CODE_SHOW in otherUserMethods) { -// // Other user wants to verify using QR code. Cross-signing has to be setup -// // val qrCodeData = createQrCodeData(transactionId, otherUserId, otherDeviceId) -// // -// // if (qrCodeData != null) { -// if (VERIFICATION_METHOD_QR_CODE_SCAN in otherUserMethods && VerificationMethod.QR_CODE_SHOW in methods) { -// // Other can Scan and I can show QR code -// result.add(VERIFICATION_METHOD_QR_CODE_SHOW) -// result.add(VERIFICATION_METHOD_RECIPROCATE) -// } -// if (VERIFICATION_METHOD_QR_CODE_SHOW in otherUserMethods && VerificationMethod.QR_CODE_SCAN in methods) { -// // Other can show and I can scan QR code -// result.add(VERIFICATION_METHOD_QR_CODE_SCAN) -// result.add(VERIFICATION_METHOD_RECIPROCATE) -// } -// // } -// -// // if (VERIFICATION_METHOD_RECIPROCATE in result) { -// // // Create the pending transaction -// // val tx = DefaultQrCodeVerificationTransaction( -// // setDeviceVerificationAction = setDeviceVerificationAction, -// // transactionId = transactionId, -// // otherUserId = otherUserId, -// // otherDeviceId = otherDeviceId, -// // crossSigningService = crossSigningService, -// // outgoingKeyRequestManager = outgoingKeyRequestManager, -// // secretShareManager = secretShareManager, -// // cryptoStore = cryptoStore, -// // qrCodeData = qrCodeData, -// // userId = userId, -// // deviceId = deviceId ?: "", -// // isIncoming = false -// // ) -// // -// // tx.transport = transportCreator.invoke(tx) -// // -// // addTransaction(tx) -// // } -// } -// -// return result.toList() -// } - -// /** -// * This string must be unique for the pair of users performing verification for the duration that the transaction is valid. -// */ -// private fun createUniqueIDForTransaction(otherUserId: String, otherDeviceID: String): String { -// return buildString { -// append(userId).append("|") -// append(deviceId).append("|") -// append(otherUserId).append("|") -// append(otherDeviceID).append("|") -// append(UUID.randomUUID().toString()) -// } -// } - -// override suspend fun transactionUpdated(tx: VerificationTransaction) { -// dispatchTxUpdated(tx) -// if (tx.state is VerificationTxState.TerminalTxState) { -// // remove -// this.removeTransaction(tx.otherUserId, tx.transactionId) -// } -// } -} diff --git a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/KotlinQRVerification.kt b/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/KotlinQRVerification.kt deleted file mode 100644 index aa61fbc674..0000000000 --- a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/KotlinQRVerification.kt +++ /dev/null @@ -1,82 +0,0 @@ -/* - * Copyright (c) 2022 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. - */ - -package org.matrix.android.sdk.internal.crypto.verification - -import kotlinx.coroutines.CompletableDeferred -import kotlinx.coroutines.channels.Channel -import org.matrix.android.sdk.api.session.crypto.verification.CancelCode -import org.matrix.android.sdk.api.session.crypto.verification.QRCodeVerificationState -import org.matrix.android.sdk.api.session.crypto.verification.QrCodeVerificationTransaction -import org.matrix.android.sdk.api.session.crypto.verification.VerificationMethod -import org.matrix.android.sdk.internal.crypto.verification.qrcode.QrCodeData -import org.matrix.android.sdk.internal.crypto.verification.qrcode.toEncodedString - -internal class KotlinQRVerification( - private val channel: Channel, - var qrCodeData: QrCodeData?, - override val method: VerificationMethod, - override val transactionId: String, - override val otherUserId: String, - override val otherDeviceId: String?, - override val isIncoming: Boolean, - var state: QRCodeVerificationState, - val isToDevice: Boolean -) : QrCodeVerificationTransaction { - - override fun state() = state - - override val qrCodeText: String? - get() = qrCodeData?.toEncodedString() -// -// var userMSKKeyToTrust: String? = null -// var deviceKeysToTrust = mutableListOf() - -// override suspend fun userHasScannedOtherQrCode(otherQrCodeText: String) { -// TODO("Not yet implemented") -// } - - override suspend fun otherUserScannedMyQrCode() { - val deferred = CompletableDeferred() - channel.send( - VerificationIntent.ActionConfirmCodeWasScanned(otherUserId, transactionId, deferred) - ) - deferred.await() - } - - override suspend fun otherUserDidNotScannedMyQrCode() { - val deferred = CompletableDeferred() - channel.send( - // TODO what cancel code?? - VerificationIntent.ActionCancel(transactionId, deferred) - ) - deferred.await() - } - - override suspend fun cancel() { - cancel(CancelCode.User) - } - - override suspend fun cancel(code: CancelCode) { - val deferred = CompletableDeferred() - channel.send( - VerificationIntent.ActionCancel(transactionId, deferred) - ) - deferred.await() - } - - override fun isToDeviceTransport() = isToDevice -} diff --git a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/KotlinSasTransaction.kt b/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/KotlinSasTransaction.kt deleted file mode 100644 index fe6d895a93..0000000000 --- a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/KotlinSasTransaction.kt +++ /dev/null @@ -1,476 +0,0 @@ -/* - * Copyright 2022 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. - */ - -package org.matrix.android.sdk.internal.crypto.verification - -import kotlinx.coroutines.CompletableDeferred -import kotlinx.coroutines.channels.Channel -import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo -import org.matrix.android.sdk.api.session.crypto.verification.CancelCode -import org.matrix.android.sdk.api.session.crypto.verification.EmojiRepresentation -import org.matrix.android.sdk.api.session.crypto.verification.SasTransactionState -import org.matrix.android.sdk.api.session.crypto.verification.SasVerificationTransaction -import org.matrix.android.sdk.api.session.crypto.verification.SasVerificationTransaction.Companion.KEY_AGREEMENT_V1 -import org.matrix.android.sdk.api.session.crypto.verification.SasVerificationTransaction.Companion.KEY_AGREEMENT_V2 -import org.matrix.android.sdk.api.session.crypto.verification.SasVerificationTransaction.Companion.KNOWN_AGREEMENT_PROTOCOLS -import org.matrix.android.sdk.api.session.crypto.verification.SasVerificationTransaction.Companion.KNOWN_HASHES -import org.matrix.android.sdk.api.session.crypto.verification.SasVerificationTransaction.Companion.KNOWN_MACS -import org.matrix.android.sdk.api.session.crypto.verification.SasVerificationTransaction.Companion.KNOWN_SHORT_CODES -import org.matrix.android.sdk.api.session.crypto.verification.SasVerificationTransaction.Companion.SAS_MAC_SHA256 -import org.matrix.android.sdk.api.session.crypto.verification.SasVerificationTransaction.Companion.SAS_MAC_SHA256_LONGKDF -import org.matrix.android.sdk.api.session.crypto.verification.VerificationMethod -import org.matrix.android.sdk.api.session.events.model.RelationType -import org.matrix.android.sdk.api.session.room.model.message.MessageVerificationAcceptContent -import org.matrix.android.sdk.api.session.room.model.message.MessageVerificationKeyContent -import org.matrix.android.sdk.api.session.room.model.message.MessageVerificationMacContent -import org.matrix.android.sdk.api.session.room.model.message.MessageVerificationReadyContent -import org.matrix.android.sdk.api.session.room.model.message.MessageVerificationStartContent -import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent -import org.matrix.android.sdk.internal.crypto.model.rest.KeyVerificationAccept -import org.matrix.android.sdk.internal.crypto.model.rest.KeyVerificationKey -import org.matrix.android.sdk.internal.crypto.model.rest.KeyVerificationMac -import org.matrix.android.sdk.internal.crypto.model.rest.KeyVerificationReady -import org.matrix.android.sdk.internal.crypto.model.rest.KeyVerificationStart -import org.matrix.android.sdk.internal.crypto.model.rest.VERIFICATION_METHOD_SAS -import org.matrix.olm.OlmSAS -import timber.log.Timber -import java.util.Locale - -internal class KotlinSasTransaction( - private val channel: Channel, - override val transactionId: String, - override val otherUserId: String, - private val myUserId: String, - private val myTrustedMSK: String?, - override var otherDeviceId: String?, - private val myDeviceId: String, - private val myDeviceFingerprint: String, - override val isIncoming: Boolean, - val startReq: ValidVerificationInfoStart.SasVerificationInfoStart? = null, - val isToDevice: Boolean, - var state: SasTransactionState, - val olmSAS: OlmSAS, -) : SasVerificationTransaction { - - override val method: VerificationMethod - get() = VerificationMethod.SAS - - companion object { - - fun sasStart(inRoom: Boolean, fromDevice: String, requestId: String): VerificationInfoStart { - return if (inRoom) { - MessageVerificationStartContent( - fromDevice = fromDevice, - hashes = KNOWN_HASHES, - keyAgreementProtocols = KNOWN_AGREEMENT_PROTOCOLS, - messageAuthenticationCodes = KNOWN_MACS, - shortAuthenticationStrings = KNOWN_SHORT_CODES, - method = VERIFICATION_METHOD_SAS, - relatesTo = RelationDefaultContent( - type = RelationType.REFERENCE, - eventId = requestId - ), - sharedSecret = null - ) - } else { - KeyVerificationStart( - fromDevice, - VERIFICATION_METHOD_SAS, - requestId, - KNOWN_AGREEMENT_PROTOCOLS, - KNOWN_HASHES, - KNOWN_MACS, - KNOWN_SHORT_CODES, - null - ) - } - } - - fun sasAccept( - inRoom: Boolean, - requestId: String, - keyAgreementProtocol: String, - hash: String, - commitment: String, - messageAuthenticationCode: String, - shortAuthenticationStrings: List, - ): VerificationInfoAccept { - return if (inRoom) { - MessageVerificationAcceptContent.create( - requestId, - keyAgreementProtocol, - hash, - commitment, - messageAuthenticationCode, - shortAuthenticationStrings - ) - } else { - KeyVerificationAccept.create( - requestId, - keyAgreementProtocol, - hash, - commitment, - messageAuthenticationCode, - shortAuthenticationStrings - ) - } - } - - fun sasReady( - inRoom: Boolean, - requestId: String, - methods: List, - fromDevice: String, - ): VerificationInfoReady { - return if (inRoom) { - MessageVerificationReadyContent.create( - requestId, - methods, - fromDevice, - ) - } else { - KeyVerificationReady( - fromDevice = fromDevice, - methods = methods, - transactionId = requestId, - ) - } - } - - fun sasKeyMessage( - inRoom: Boolean, - requestId: String, - pubKey: String, - ): VerificationInfoKey { - return if (inRoom) { - MessageVerificationKeyContent.create(tid = requestId, pubKey = pubKey) - } else { - KeyVerificationKey.create(tid = requestId, pubKey = pubKey) - } - } - - fun sasMacMessage( - inRoom: Boolean, - requestId: String, - validVerificationInfoMac: ValidVerificationInfoMac - ): VerificationInfoMac { - return if (inRoom) { - MessageVerificationMacContent.create( - tid = requestId, - keys = validVerificationInfoMac.keys, - mac = validVerificationInfoMac.mac - ) - } else { - KeyVerificationMac.create( - tid = requestId, - keys = validVerificationInfoMac.keys, - mac = validVerificationInfoMac.mac - ) - } - } - } - - override fun toString(): String { - return "KotlinSasTransaction(transactionId=$transactionId, state=$state, otherUserId=$otherUserId, otherDeviceId=$otherDeviceId, isToDevice=$isToDevice)" - } - - // To override finalize(), all you need to do is simply declare it, without using the override keyword: - protected fun finalize() { - releaseSAS() - } - - private fun releaseSAS() { - // finalization logic - olmSAS.releaseSas() - } - - var accepted: ValidVerificationInfoAccept? = null - var otherKey: String? = null - var shortCodeBytes: ByteArray? = null - var myMac: ValidVerificationInfoMac? = null - var theirMac: ValidVerificationInfoMac? = null - var verifiedSuccessInfo: MacVerificationResult.Success? = null - - override fun state() = this.state - -// override fun supportsEmoji(): Boolean { -// return accepted?.shortAuthenticationStrings?.contains(SasMode.EMOJI) == true -// } - - override fun getEmojiCodeRepresentation(): List { - return shortCodeBytes?.getEmojiCodeRepresentation().orEmpty() - } - - override fun getDecimalCodeRepresentation(): String? { - return shortCodeBytes?.getDecimalCodeRepresentation() - } - - override suspend fun userHasVerifiedShortCode() { - val deferred = CompletableDeferred() - channel.send( - VerificationIntent.ActionSASCodeMatches(transactionId, deferred) - ) - deferred.await() - } - - override suspend fun acceptVerification() { - // nop - // as we are using verification request accept is automatic - } - - override suspend fun shortCodeDoesNotMatch() { - val deferred = CompletableDeferred() - channel.send( - VerificationIntent.ActionSASCodeDoesNotMatch(transactionId, deferred) - ) - deferred.await() - } - - override suspend fun cancel() { - val deferred = CompletableDeferred() - channel.send( - VerificationIntent.ActionCancel(transactionId, deferred) - ) - deferred.await() - } - - override suspend fun cancel(code: CancelCode) { - val deferred = CompletableDeferred() - channel.send( - VerificationIntent.ActionCancel(transactionId, deferred) - ) - deferred.await() - } - - override fun isToDeviceTransport() = isToDevice - - fun calculateSASBytes(otherKey: String) { - this.otherKey = otherKey - olmSAS.setTheirPublicKey(otherKey) - shortCodeBytes = when (accepted!!.keyAgreementProtocol) { - KEY_AGREEMENT_V1 -> { - // (Note: In all of the following HKDF is as defined in RFC 5869, and uses the previously agreed-on hash function as the hash function, - // the shared secret as the input keying material, no salt, and with the input parameter set to the concatenation of: - // - the string “MATRIX_KEY_VERIFICATION_SAS”, - // - the Matrix ID of the user who sent the m.key.verification.start message, - // - the device ID of the device that sent the m.key.verification.start message, - // - the Matrix ID of the user who sent the m.key.verification.accept message, - // - he device ID of the device that sent the m.key.verification.accept message - // - the transaction ID. - val sasInfo = buildString { - append("MATRIX_KEY_VERIFICATION_SAS") - if (isIncoming) { - append(otherUserId) - append(otherDeviceId) - append(myUserId) - append(myDeviceId) - append(olmSAS.publicKey) - } else { - append(myUserId) - append(myDeviceId) - append(otherUserId) - append(otherDeviceId) - } - append(transactionId) - } - // decimal: generate five bytes by using HKDF. - // emoji: generate six bytes by using HKDF. - olmSAS.generateShortCode(sasInfo, 6) - } - KEY_AGREEMENT_V2 -> { - val sasInfo = buildString { - append("MATRIX_KEY_VERIFICATION_SAS|") - if (isIncoming) { - append(otherUserId).append('|') - append(otherDeviceId).append('|') - append(otherKey).append('|') - append(myUserId).append('|') - append(myDeviceId).append('|') - append(olmSAS.publicKey).append('|') - } else { - append(myUserId).append('|') - append(myDeviceId).append('|') - append(olmSAS.publicKey).append('|') - append(otherUserId).append('|') - append(otherDeviceId).append('|') - append(otherKey).append('|') - } - append(transactionId) - } - olmSAS.generateShortCode(sasInfo, 6) - } - else -> { - // Protocol has been checked earlier - throw IllegalArgumentException() - } - } - } - - fun computeMyMac(): ValidVerificationInfoMac { - val baseInfo = buildString { - append("MATRIX_KEY_VERIFICATION_MAC") - append(myUserId) - append(myDeviceId) - append(otherUserId) - append(otherDeviceId) - append(transactionId) - } - - // Previously, with SAS verification, the m.key.verification.mac message only contained the user's device key. - // It should now contain both the device key and the MSK. - // So when Alice and Bob verify with SAS, the verification will verify the MSK. - - val keyMap = HashMap() - - val keyId = "ed25519:$myDeviceId" - val macString = macUsingAgreedMethod(myDeviceFingerprint, baseInfo + keyId) - - if (macString.isNullOrBlank()) { - // Should not happen - Timber.e("## SAS verification [$transactionId] failed to send KeyMac, empty key hashes.") - throw IllegalStateException("Invalid mac for transaction ${transactionId}") - } - - keyMap[keyId] = macString - - if (myTrustedMSK != null) { - val crossSigningKeyId = "ed25519:$myTrustedMSK" - macUsingAgreedMethod(myTrustedMSK, baseInfo + crossSigningKeyId)?.let { mskMacString -> - keyMap[crossSigningKeyId] = mskMacString - } - } - - val keyStrings = macUsingAgreedMethod(keyMap.keys.sorted().joinToString(","), baseInfo + "KEY_IDS") - - if (keyStrings.isNullOrBlank()) { - // Should not happen - Timber.e("## SAS verification [$transactionId] failed to send KeyMac, empty key hashes.") - throw IllegalStateException("Invalid key mac for transaction ${transactionId}") - } - - return ValidVerificationInfoMac( - transactionId, - keyMap, - keyStrings - ).also { - myMac = it - } - } - - sealed class MacVerificationResult { - - object MismatchKeys : MacVerificationResult() - data class MismatchMacDevice(val deviceId: String) : MacVerificationResult() - object MismatchMacCrossSigning : MacVerificationResult() - object NoDevicesVerified : MacVerificationResult() - - data class Success(val verifiedDeviceId: List, val otherMskTrusted: Boolean) : MacVerificationResult() - } - - fun verifyMacs( - theirMacSafe: ValidVerificationInfoMac, - otherUserKnownDevices: List, - otherMasterKey: String? - ): MacVerificationResult { - Timber.v("## SAS verifying macs for id:$transactionId") - - // Bob’s device calculates the HMAC (as above) of its copies of Alice’s keys given in the message (as identified by their key ID), - // as well as the HMAC of the comma-separated, sorted list of the key IDs given in the message. - // Bob’s device compares these with the HMAC values given in the m.key.verification.mac message. - // If everything matches, then consider Alice’s device keys as verified. - val baseInfo = buildString { - append("MATRIX_KEY_VERIFICATION_MAC") - append(otherUserId) - append(otherDeviceId) - append(myUserId) - append(myDeviceId) - append(transactionId) - } - - val commaSeparatedListOfKeyIds = theirMacSafe.mac.keys.sorted().joinToString(",") - - val keyStrings = macUsingAgreedMethod(commaSeparatedListOfKeyIds, baseInfo + "KEY_IDS") - if (theirMacSafe.keys != keyStrings) { - // WRONG! - return MacVerificationResult.MismatchKeys - } - - val verifiedDevices = ArrayList() - - // cannot be empty because it has been validated - theirMacSafe.mac.keys.forEach { entry -> - val keyIDNoPrefix = entry.removePrefix("ed25519:") - val otherDeviceKey = otherUserKnownDevices - .firstOrNull { it.deviceId == keyIDNoPrefix } - ?.fingerprint() - if (otherDeviceKey == null) { - Timber.w("## SAS Verification: Could not find device $keyIDNoPrefix to verify") - // just ignore and continue - return@forEach - } - val mac = macUsingAgreedMethod(otherDeviceKey, baseInfo + entry) - if (mac != theirMacSafe.mac[entry]) { - // WRONG! - Timber.e("## SAS Verification: mac mismatch for $otherDeviceKey with id $keyIDNoPrefix") - // cancel(CancelCode.MismatchedKeys) - return MacVerificationResult.MismatchMacDevice(keyIDNoPrefix) - } - verifiedDevices.add(keyIDNoPrefix) - } - - var otherMasterKeyIsVerified = false - if (otherMasterKey != null) { - // Did the user signed his master key - theirMacSafe.mac.keys.forEach { - val keyIDNoPrefix = it.removePrefix("ed25519:") - if (keyIDNoPrefix == otherMasterKey) { - // Check the signature - val mac = macUsingAgreedMethod(otherMasterKey, baseInfo + it) - if (mac != theirMacSafe.mac[it]) { - // WRONG! - Timber.e("## SAS Verification: mac mismatch for MasterKey with id $keyIDNoPrefix") - return MacVerificationResult.MismatchMacCrossSigning - } else { - otherMasterKeyIsVerified = true - } - } - } - } - - // if none of the keys could be verified, then error because the app - // should be informed about that - if (verifiedDevices.isEmpty() && !otherMasterKeyIsVerified) { - Timber.e("## SAS Verification: No devices verified") - return MacVerificationResult.NoDevicesVerified - } - - return MacVerificationResult.Success( - verifiedDevices, - otherMasterKeyIsVerified - ).also { - // store and will persist when transaction is actually done - verifiedSuccessInfo = it - } - } - - private fun macUsingAgreedMethod(message: String, info: String): String? { - return when (accepted?.messageAuthenticationCode?.lowercase(Locale.ROOT)) { - SAS_MAC_SHA256_LONGKDF -> olmSAS.calculateMacLongKdf(message, info) - SAS_MAC_SHA256 -> olmSAS.calculateMac(message, info) - else -> null - } - } -} diff --git a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/KotlinVerificationRequest.kt b/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/KotlinVerificationRequest.kt deleted file mode 100644 index b55607b3d8..0000000000 --- a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/KotlinVerificationRequest.kt +++ /dev/null @@ -1,131 +0,0 @@ -/* - * Copyright (c) 2022 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. - */ - -package org.matrix.android.sdk.internal.crypto.verification - -import org.matrix.android.sdk.api.extensions.orFalse -import org.matrix.android.sdk.api.session.crypto.verification.CancelCode -import org.matrix.android.sdk.api.session.crypto.verification.EVerificationState -import org.matrix.android.sdk.api.session.crypto.verification.PendingVerificationRequest -import org.matrix.android.sdk.api.session.crypto.verification.ValidVerificationInfoReady -import org.matrix.android.sdk.api.session.crypto.verification.ValidVerificationInfoRequest -import org.matrix.android.sdk.internal.crypto.model.rest.VERIFICATION_METHOD_QR_CODE_SCAN -import org.matrix.android.sdk.internal.crypto.model.rest.VERIFICATION_METHOD_QR_CODE_SHOW -import org.matrix.android.sdk.internal.crypto.model.rest.VERIFICATION_METHOD_SAS -import org.matrix.android.sdk.internal.crypto.verification.qrcode.QrCodeData -import org.matrix.android.sdk.internal.crypto.verification.qrcode.toEncodedString - -internal class KotlinVerificationRequest( - val requestId: String, - val incoming: Boolean, - val otherUserId: String, - var state: EVerificationState, - val ageLocalTs: Long -) { - - var roomId: String? = null - var qrCodeData: QrCodeData? = null - var targetDevices: List? = null - var requestInfo: ValidVerificationInfoRequest? = null - var readyInfo: ValidVerificationInfoReady? = null - var cancelCode: CancelCode? = null - -// fun requestId() = requestId -// -// fun incoming() = incoming -// -// fun otherUserId() = otherUserId -// -// fun roomId() = roomId -// -// fun targetDevices() = targetDevices -// -// fun state() = state -// -// fun ageLocalTs() = ageLocalTs - - fun otherDeviceId(): String? { - return if (incoming) { - requestInfo?.fromDevice - } else { - readyInfo?.fromDevice - } - } - - fun cancelCode(): CancelCode? = cancelCode - - /** - * SAS is supported if I support it and the other party support it. - */ - private fun isSasSupported(): Boolean { - return requestInfo?.methods?.contains(VERIFICATION_METHOD_SAS).orFalse() && - readyInfo?.methods?.contains(VERIFICATION_METHOD_SAS).orFalse() - } - - /** - * Other can show QR code if I can scan QR code and other can show QR code. - */ - private fun otherCanShowQrCode(): Boolean { - return if (incoming) { - requestInfo?.methods?.contains(VERIFICATION_METHOD_QR_CODE_SHOW).orFalse() && - readyInfo?.methods?.contains(VERIFICATION_METHOD_QR_CODE_SCAN).orFalse() - } else { - requestInfo?.methods?.contains(VERIFICATION_METHOD_QR_CODE_SCAN).orFalse() && - readyInfo?.methods?.contains(VERIFICATION_METHOD_QR_CODE_SHOW).orFalse() - } - } - - /** - * Other can scan QR code if I can show QR code and other can scan QR code. - */ - private fun otherCanScanQrCode(): Boolean { - return if (incoming) { - requestInfo?.methods?.contains(VERIFICATION_METHOD_QR_CODE_SCAN).orFalse() && - readyInfo?.methods?.contains(VERIFICATION_METHOD_QR_CODE_SHOW).orFalse() - } else { - requestInfo?.methods?.contains(VERIFICATION_METHOD_QR_CODE_SHOW).orFalse() && - readyInfo?.methods?.contains(VERIFICATION_METHOD_QR_CODE_SCAN).orFalse() - } - } - - fun qrCodeText() = qrCodeData?.toEncodedString() - - override fun toString(): String { - return toPendingVerificationRequest().toString() - } - - fun toPendingVerificationRequest(): PendingVerificationRequest { - return PendingVerificationRequest( - ageLocalTs = ageLocalTs, - state = state, - isIncoming = incoming, - otherUserId = otherUserId, - roomId = roomId, - transactionId = requestId, - cancelConclusion = cancelCode, - isFinished = isFinished(), - handledByOtherSession = state == EVerificationState.HandledByOtherSession, - targetDevices = targetDevices, - qrCodeText = qrCodeText(), - isSasSupported = isSasSupported(), - weShouldShowScanOption = otherCanShowQrCode(), - weShouldDisplayQRCode = otherCanScanQrCode(), - otherDeviceId = otherDeviceId() - ) - } - - fun isFinished() = state == EVerificationState.Cancelled || state == EVerificationState.Done -} diff --git a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/VerificationActor.kt b/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/VerificationActor.kt deleted file mode 100644 index dff2fe921b..0000000000 --- a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/VerificationActor.kt +++ /dev/null @@ -1,1749 +0,0 @@ -/* - * Copyright 2022 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. - */ - -package org.matrix.android.sdk.internal.crypto.verification - -import androidx.annotation.VisibleForTesting -import dagger.assisted.Assisted -import dagger.assisted.AssistedFactory -import dagger.assisted.AssistedInject -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.channels.BufferOverflow -import kotlinx.coroutines.channels.Channel -import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.launch -import org.matrix.android.sdk.BuildConfig -import org.matrix.android.sdk.api.extensions.orFalse -import org.matrix.android.sdk.api.extensions.tryOrNull -import org.matrix.android.sdk.api.logger.LoggerTag -import org.matrix.android.sdk.api.session.crypto.crosssigning.KEYBACKUP_SECRET_SSSS_NAME -import org.matrix.android.sdk.api.session.crypto.crosssigning.MASTER_KEY_SSSS_NAME -import org.matrix.android.sdk.api.session.crypto.crosssigning.SELF_SIGNING_KEY_SSSS_NAME -import org.matrix.android.sdk.api.session.crypto.crosssigning.USER_SIGNING_KEY_SSSS_NAME -import org.matrix.android.sdk.api.session.crypto.verification.CancelCode -import org.matrix.android.sdk.api.session.crypto.verification.EVerificationState -import org.matrix.android.sdk.api.session.crypto.verification.QRCodeVerificationState -import org.matrix.android.sdk.api.session.crypto.verification.SasTransactionState -import org.matrix.android.sdk.api.session.crypto.verification.SasVerificationTransaction -import org.matrix.android.sdk.api.session.crypto.verification.ValidVerificationInfoReady -import org.matrix.android.sdk.api.session.crypto.verification.ValidVerificationInfoRequest -import org.matrix.android.sdk.api.session.crypto.verification.VerificationEvent -import org.matrix.android.sdk.api.session.crypto.verification.VerificationMethod -import org.matrix.android.sdk.api.session.crypto.verification.VerificationTransaction -import org.matrix.android.sdk.api.session.crypto.verification.safeValueOf -import org.matrix.android.sdk.api.session.events.model.EventType -import org.matrix.android.sdk.api.session.events.model.LocalEcho -import org.matrix.android.sdk.api.session.events.model.RelationType -import org.matrix.android.sdk.api.session.events.model.toContent -import org.matrix.android.sdk.api.session.room.model.message.MessageVerificationCancelContent -import org.matrix.android.sdk.api.session.room.model.message.MessageVerificationDoneContent -import org.matrix.android.sdk.api.session.room.model.message.MessageVerificationRequestContent -import org.matrix.android.sdk.api.session.room.model.message.MessageVerificationStartContent -import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent -import org.matrix.android.sdk.internal.crypto.SecretShareManager -import org.matrix.android.sdk.internal.crypto.model.rest.KeyVerificationCancel -import org.matrix.android.sdk.internal.crypto.model.rest.KeyVerificationDone -import org.matrix.android.sdk.internal.crypto.model.rest.KeyVerificationRequest -import org.matrix.android.sdk.internal.crypto.model.rest.KeyVerificationStart -import org.matrix.android.sdk.internal.crypto.model.rest.VERIFICATION_METHOD_QR_CODE_SCAN -import org.matrix.android.sdk.internal.crypto.model.rest.VERIFICATION_METHOD_QR_CODE_SHOW -import org.matrix.android.sdk.internal.crypto.model.rest.VERIFICATION_METHOD_RECIPROCATE -import org.matrix.android.sdk.internal.crypto.model.rest.VERIFICATION_METHOD_SAS -import org.matrix.android.sdk.internal.crypto.model.rest.toValue -import org.matrix.android.sdk.internal.crypto.verification.qrcode.QrCodeData -import org.matrix.android.sdk.internal.crypto.verification.qrcode.generateSharedSecretV2 -import org.matrix.android.sdk.internal.crypto.verification.qrcode.toQrCodeData -import org.matrix.android.sdk.internal.di.UserId -import org.matrix.android.sdk.internal.util.time.Clock -import timber.log.Timber -import java.util.Locale - -private val loggerTag = LoggerTag("Verification", LoggerTag.CRYPTO) - -internal class VerificationActor @AssistedInject constructor( - @Assisted private val scope: CoroutineScope, - private val clock: Clock, - @UserId private val myUserId: String, - private val secretShareManager: SecretShareManager, - private val transportLayer: VerificationTransportLayer, - private val verificationRequestsStore: VerificationRequestsStore, - private val olmPrimitiveProvider: VerificationCryptoPrimitiveProvider, - private val verificationTrustBackend: VerificationTrustBackend, -) { - - @AssistedFactory - interface Factory { - fun create(scope: CoroutineScope): VerificationActor - } - - @VisibleForTesting - val channel = Channel( - capacity = Channel.UNLIMITED, - ) - - init { - scope.launch { - for (msg in channel) { - onReceive(msg) - } - } - } - - // Replaces the typical list of listeners pattern. - // Looks to me as the sane setup, not sure if more than 1 is needed as extraBufferCapacity - val eventFlow = MutableSharedFlow(extraBufferCapacity = 20, onBufferOverflow = BufferOverflow.SUSPEND) - - suspend fun send(intent: VerificationIntent) { - channel.send(intent) - } - - private suspend fun withMatchingRequest( - otherUserId: String, - requestId: String, - block: suspend ((KotlinVerificationRequest) -> Unit) - ) { - val matchingRequest = verificationRequestsStore.getExistingRequest(otherUserId, requestId) - ?: return Unit.also { - // Receive a transaction event with no matching request.. should ignore. - // Not supported any more to do raw start - Timber.tag(loggerTag.value) - .v("[${myUserId.take(8)}] request $requestId not found!") - } - - if (matchingRequest.state == EVerificationState.HandledByOtherSession) { - Timber.tag(loggerTag.value) - .v("[${myUserId.take(8)}] ignore transaction event for $requestId handled by other") - return - } - - if (matchingRequest.isFinished()) { - Timber.tag(loggerTag.value) - .v("[${myUserId.take(8)}] ignore transaction event for $requestId for finished request") - return - } - block.invoke(matchingRequest) - } - - private suspend fun withMatchingRequest( - otherUserId: String, - requestId: String, - viaRoom: String?, - block: suspend ((KotlinVerificationRequest) -> Unit) - ) { - val matchingRequest = verificationRequestsStore.getExistingRequest(otherUserId, requestId) - ?: return Unit.also { - // Receive a transaction event with no matching request.. should ignore. - // Not supported any more to do raw start - Timber.tag(loggerTag.value) - .v("[${myUserId.take(8)}] request $requestId not found!") - } - - if (matchingRequest.state == EVerificationState.HandledByOtherSession) { - Timber.tag(loggerTag.value) - .v("[${myUserId.take(8)}] ignore transaction event for $requestId handled by other") - return - } - - if (matchingRequest.isFinished()) { - Timber.tag(loggerTag.value) - .v("[${myUserId.take(8)}] ignore transaction event for $requestId for finished request") - return - } - - if (viaRoom == null && matchingRequest.roomId != null) { - // mismatch transport - return Unit.also { - Timber.v("Mismatch transport: received to device for in room verification id:${requestId}") - } - } else if (viaRoom != null && matchingRequest.roomId != viaRoom) { - // mismatch transport or room - return Unit.also { - Timber.v("Mismatch transport: received in room ${viaRoom} for verification id:${requestId} in room ${matchingRequest.roomId}") - } - } - - block(matchingRequest) - } - - suspend fun onReceive(msg: VerificationIntent) { - Timber.tag(loggerTag.value) - .v("[${myUserId.take(8)}]: $msg") - when (msg) { - is VerificationIntent.ActionRequestVerification -> { - handleActionRequestVerification(msg) - } - is VerificationIntent.OnReadyReceived -> { - handleReadyReceived(msg) - } -// is VerificationIntent.UpdateRequest -> { -// updatePendingRequest(msg.request) -// } - is VerificationIntent.GetExistingRequestInRoom -> { - val existing = verificationRequestsStore.getExistingRequestInRoom(msg.transactionId, msg.roomId) - msg.deferred.complete(existing?.toPendingVerificationRequest()) - } - is VerificationIntent.OnVerificationRequestReceived -> { - handleIncomingRequest(msg) - } - is VerificationIntent.ActionReadyRequest -> { - handleActionReadyRequest(msg) - } - is VerificationIntent.ActionStartSasVerification -> { - handleSasStart(msg) - } - is VerificationIntent.ActionReciprocateQrVerification -> { - handleActionReciprocateQR(msg) - } - is VerificationIntent.ActionConfirmCodeWasScanned -> { - withMatchingRequest(msg.otherUserId, msg.requestId) { - handleActionQRScanConfirmed(it) - } - msg.deferred.complete(Unit) - } - is VerificationIntent.OnStartReceived -> { - onStartReceived(msg) - } - is VerificationIntent.OnAcceptReceived -> { - withMatchingRequest(msg.fromUser, msg.validAccept.transactionId, msg.viaRoom) { - handleReceiveAccept(it, msg) - } - } - is VerificationIntent.OnKeyReceived -> { - withMatchingRequest(msg.fromUser, msg.validKey.transactionId, msg.viaRoom) { - handleReceiveKey(it, msg) - } - } - is VerificationIntent.ActionSASCodeDoesNotMatch -> { - handleSasCodeDoesNotMatch(msg) - } - is VerificationIntent.ActionSASCodeMatches -> { - handleSasCodeMatch(msg) - } - is VerificationIntent.OnMacReceived -> { - withMatchingRequest(msg.fromUser, msg.validMac.transactionId, msg.viaRoom) { - handleMacReceived(it, msg) - } - } - is VerificationIntent.OnDoneReceived -> { - withMatchingRequest(msg.fromUser, msg.transactionId, msg.viaRoom) { - handleDoneReceived(it, msg) - } - } - is VerificationIntent.ActionCancel -> { - verificationRequestsStore.getExistingRequestWithRequestId(msg.transactionId) - ?.let { matchingRequest -> - try { - cancelRequest(matchingRequest, CancelCode.User) - msg.deferred.complete(Unit) - } catch (failure: Throwable) { - msg.deferred.completeExceptionally(failure) - } - } - } - is VerificationIntent.OnUnableToDecryptVerificationEvent -> { - // at least if request was sent by me, I can safely cancel without interfering - val matchingRequest = verificationRequestsStore.getExistingRequest(msg.fromUser, msg.transactionId) - ?: return - if (matchingRequest.state != EVerificationState.HandledByOtherSession) { - cancelRequest(matchingRequest, CancelCode.InvalidMessage) - } - } - is VerificationIntent.GetExistingRequestsForUser -> { - verificationRequestsStore.getExistingRequestsForUser(msg.userId).let { requests -> - Timber.tag(loggerTag.value) - .v("[${myUserId.take(8)}]: Found $requests") - msg.deferred.complete(requests.map { it.toPendingVerificationRequest() }) - } - } - is VerificationIntent.GetExistingTransaction -> { - verificationRequestsStore - .getExistingTransaction(msg.fromUser, msg.transactionId) - .let { - msg.deferred.complete(it) - } - } - is VerificationIntent.GetExistingRequest -> { - verificationRequestsStore - .getExistingRequest(msg.otherUserId, msg.transactionId) - .let { - msg.deferred.complete(it?.toPendingVerificationRequest()) - } - } - is VerificationIntent.OnCancelReceived -> { - withMatchingRequest(msg.fromUser, msg.validCancel.transactionId, msg.viaRoom) { request -> - // update as canceled - request.state = EVerificationState.Cancelled - val cancelCode = safeValueOf(msg.validCancel.code) - request.cancelCode = cancelCode - // TODO or QR - val existingTx: KotlinSasTransaction? = - getExistingTransaction(msg.validCancel.transactionId) // txMap[msg.fromUser]?.get(msg.validCancel.transactionId) - if (existingTx != null) { - existingTx.state = SasTransactionState.Cancelled(cancelCode, false) - verificationRequestsStore.deleteTransaction(msg.fromUser, msg.validCancel.transactionId) - dispatchUpdate(VerificationEvent.TransactionUpdated(existingTx)) - } - dispatchUpdate(VerificationEvent.RequestUpdated(request.toPendingVerificationRequest())) - } - } - is VerificationIntent.OnReadyByAnotherOfMySessionReceived -> { - handleReadyByAnotherOfMySessionReceived(msg) - } - } - } - - private fun dispatchUpdate(update: VerificationEvent) { - // We don't want to block on emit. - // If no subscriber there is a small buffer - Timber.tag(loggerTag.value) - .v("[${myUserId.take(8)}] Dispatch Request update ${update.transactionId}") - scope.launch { - eventFlow.emit(update) - } - } - - private suspend fun handleIncomingRequest(msg: VerificationIntent.OnVerificationRequestReceived) { - val pendingVerificationRequest = KotlinVerificationRequest( - requestId = msg.validRequestInfo.transactionId, - incoming = true, - otherUserId = msg.senderId, - state = EVerificationState.Requested, - ageLocalTs = msg.timeStamp ?: clock.epochMillis() - ).apply { - requestInfo = msg.validRequestInfo - roomId = msg.roomId - } - verificationRequestsStore.addRequest(msg.senderId, pendingVerificationRequest) - dispatchRequestAdded(pendingVerificationRequest) - } - - private suspend fun onStartReceived(msg: VerificationIntent.OnStartReceived) { - val requestId = msg.validVerificationInfoStart.transactionId - val matchingRequest = verificationRequestsStore - .getExistingRequestWithRequestId(msg.validVerificationInfoStart.transactionId) - ?: return Unit.also { - // Receive a start with no matching request.. should ignore. - // Not supported any more to do raw start - Timber.tag(loggerTag.value) - .v("[${myUserId.take(8)}] Start for request $requestId not found!") - } - - if (matchingRequest.state == EVerificationState.HandledByOtherSession) { - // ignore - return - } - if (matchingRequest.state != EVerificationState.Ready) { - cancelRequest(matchingRequest, CancelCode.UnexpectedMessage) - return - } - - if (msg.viaRoom == null && matchingRequest.roomId != null) { - // mismatch transport - return Unit.also { - Timber.v("onStartReceived in to device for in room verification id:${requestId}") - } - } else if (msg.viaRoom != null && matchingRequest.roomId != msg.viaRoom) { - // mismatch transport or room - return Unit.also { - Timber.v("onStartReceived in room ${msg.viaRoom} for verification id:${requestId} in room ${matchingRequest.roomId}") - } - } - - when (msg.validVerificationInfoStart) { - is ValidVerificationInfoStart.ReciprocateVerificationInfoStart -> { - handleReceiveStartForQR(matchingRequest, msg.validVerificationInfoStart) - } - is ValidVerificationInfoStart.SasVerificationInfoStart -> { - handleReceiveStartForSas( - msg, - matchingRequest, - msg.validVerificationInfoStart - ) - } - } - matchingRequest.state = EVerificationState.Started - dispatchUpdate(VerificationEvent.RequestUpdated(matchingRequest.toPendingVerificationRequest())) - } - - private suspend fun handleReceiveStartForQR(request: KotlinVerificationRequest, reciprocate: ValidVerificationInfoStart.ReciprocateVerificationInfoStart) { - // Ok so the other did scan our code - val ourSecret = request.qrCodeData?.sharedSecret - if (ourSecret != reciprocate.sharedSecret) { - // something went wrong - cancelRequest(request, CancelCode.MismatchedKeys) - return - } - - // The secret matches, we need manual action to confirm that it was scan - val tx = KotlinQRVerification( - channel = this.channel, - state = QRCodeVerificationState.WaitingForScanConfirmation, - qrCodeData = request.qrCodeData, - method = VerificationMethod.QR_CODE_SCAN, - transactionId = request.requestId, - otherUserId = request.otherUserId, - otherDeviceId = request.otherDeviceId(), - isIncoming = false, - isToDevice = request.roomId == null - ) - addTransaction(tx) - } - - private suspend fun handleReceiveStartForSas( - msg: VerificationIntent.OnStartReceived, - request: KotlinVerificationRequest, - sasStart: ValidVerificationInfoStart.SasVerificationInfoStart - ) { - Timber.tag(loggerTag.value) - .v("[${myUserId.take(8)}] Incoming SAS start for request ${request.requestId}") - // start is a bit special as it could be started from both side - // the event sent by the user whose user ID is the smallest is used, - // and the other m.key.verification.start event is ignored. - // So let's check if I already send a start? - val requestId = msg.validVerificationInfoStart.transactionId - val existing: KotlinSasTransaction? = getExistingTransaction(msg.fromUser, requestId) - if (existing != null) { - Timber.tag(loggerTag.value) - .v("[${myUserId.take(8)}] No existing Sas transaction for ${request.requestId}") - tryOrNull { cancelRequest(request, CancelCode.UnexpectedMessage) } - return - } - - // we accept with the agreement methods - // Select a key agreement protocol, a hash algorithm, a message authentication code, - // and short authentication string methods out of the lists given in requester's message. - // TODO create proper exceptions and catch in caller - val agreedProtocol = sasStart.keyAgreementProtocols.firstOrNull { SasVerificationTransaction.KNOWN_AGREEMENT_PROTOCOLS.contains(it) } - ?: return Unit.also { - Timber.e("## protocol agreement error for request ${request.requestId}") - cancelRequest(request, CancelCode.UnknownMethod) - } - val agreedHash = sasStart.hashes.firstOrNull { SasVerificationTransaction.KNOWN_HASHES.contains(it) } - ?: return Unit.also { - Timber.e("## hash agreement error for request ${request.requestId}") - cancelRequest(request, CancelCode.UserError) - } - val agreedMac = sasStart.messageAuthenticationCodes.firstOrNull { SasVerificationTransaction.KNOWN_MACS.contains(it) } - ?: return Unit.also { - Timber.e("## sas agreement error for request ${request.requestId}") - cancelRequest(request, CancelCode.UserError) - } - val agreedShortCode = sasStart.shortAuthenticationStrings - .filter { SasVerificationTransaction.KNOWN_SHORT_CODES.contains(it) } - .takeIf { it.isNotEmpty() } - ?: return Unit.also { - Timber.e("## SAS agreement error for request ${request.requestId}") - cancelRequest(request, CancelCode.UserError) - } - - val otherDeviceId = request.otherDeviceId() - ?: return Unit.also { - Timber.e("## SAS Unexpected method") - cancelRequest(request, CancelCode.UnknownMethod) - } - // Bob’s device ensures that it has a copy of Alice’s device key. - val mxDeviceInfo = verificationTrustBackend.getUserDevice(request.otherUserId, otherDeviceId) - - if (mxDeviceInfo?.fingerprint() == null) { - Timber.e("## SAS Failed to find device key ") - // TODO force download keys!! - // would be probably better to download the keys - // for now I cancel - cancelRequest(request, CancelCode.UserError) - return - } - val sasTx = KotlinSasTransaction( - channel = channel, - transactionId = requestId, - state = SasTransactionState.None, - otherUserId = request.otherUserId, - myUserId = myUserId, - myTrustedMSK = verificationTrustBackend.getMyTrustedMasterKeyBase64(), - otherDeviceId = request.otherDeviceId(), - myDeviceId = verificationTrustBackend.getMyDeviceId(), - myDeviceFingerprint = verificationTrustBackend.getMyDevice().fingerprint().orEmpty(), - startReq = sasStart, - isIncoming = true, - isToDevice = msg.viaRoom == null, - olmSAS = olmPrimitiveProvider.provideOlmSas() - ) - - val concat = sasTx.olmSAS.publicKey + sasStart.canonicalJson - val commitment = hashUsingAgreedHashMethod(agreedHash, concat) - - val accept = KotlinSasTransaction.sasAccept( - inRoom = request.roomId != null, - requestId = requestId, - keyAgreementProtocol = agreedProtocol, - hash = agreedHash, - messageAuthenticationCode = agreedMac, - shortAuthenticationStrings = agreedShortCode, - commitment = commitment - ) - - // cancel if network error (would not send back a cancel but at least current user will see feedback?) - try { - transportLayer.sendToOther(request, EventType.KEY_VERIFICATION_ACCEPT, accept) - } catch (failure: Throwable) { - Timber.tag(loggerTag.value) - .v("[${myUserId.take(8)}] Failed to send accept for ${request.requestId}") - tryOrNull { cancelRequest(request, CancelCode.User) } - } - - sasTx.accepted = accept.asValidObject() - sasTx.state = SasTransactionState.SasAccepted - - addTransaction(sasTx) - } - - private suspend fun handleReceiveAccept(matchingRequest: KotlinVerificationRequest, msg: VerificationIntent.OnAcceptReceived) { - val requestId = msg.validAccept.transactionId - - val existing: KotlinSasTransaction = getExistingTransaction(msg.fromUser, requestId) - ?: return Unit.also { - Timber.v("on accept received in room ${msg.viaRoom} for verification id:${requestId} in room ${matchingRequest.roomId}") - } - - // Existing should be in - if (existing.state != SasTransactionState.SasStarted) { - // it's a wrong state should cancel? - // TODO cancel - } - - val accept = msg.validAccept - // Check that the agreement is correct - if (!SasVerificationTransaction.KNOWN_AGREEMENT_PROTOCOLS.contains(accept.keyAgreementProtocol) || - !SasVerificationTransaction.KNOWN_HASHES.contains(accept.hash) || - !SasVerificationTransaction.KNOWN_MACS.contains(accept.messageAuthenticationCode) || - accept.shortAuthenticationStrings.intersect(SasVerificationTransaction.KNOWN_SHORT_CODES).isEmpty()) { - Timber.e("## SAS agreement error for request ${matchingRequest.requestId}") - cancelRequest(matchingRequest, CancelCode.UnknownMethod) - return - } - - // Upon receipt of the m.key.verification.accept message from Bob’s device, - // Alice’s device stores the commitment value for later use. - - // Alice’s device creates an ephemeral Curve25519 key pair (dA,QA), - // and replies with a to_device message with type set to “m.key.verification.key”, sending Alice’s public key QA - val pubKey = existing.olmSAS.publicKey - - val keyMessage = KotlinSasTransaction.sasKeyMessage(matchingRequest.roomId != null, requestId, pubKey) - - try { - if (BuildConfig.LOG_PRIVATE_DATA) { - Timber.tag(loggerTag.value) - .v("[${myUserId.take(8)}]: Sending my key $pubKey") - } - transportLayer.sendToOther( - matchingRequest, - EventType.KEY_VERIFICATION_KEY, - keyMessage, - ) - } catch (failure: Throwable) { - existing.state = SasTransactionState.Cancelled(CancelCode.UserError, true) - matchingRequest.cancelCode = CancelCode.UserError - matchingRequest.state = EVerificationState.Cancelled - dispatchUpdate(VerificationEvent.RequestUpdated(matchingRequest.toPendingVerificationRequest())) - dispatchUpdate(VerificationEvent.TransactionUpdated(existing)) - return - } - existing.accepted = accept - existing.state = SasTransactionState.SasKeySent - dispatchUpdate(VerificationEvent.TransactionUpdated(existing)) - } - - private suspend fun handleSasStart(msg: VerificationIntent.ActionStartSasVerification) { - val matchingRequest = verificationRequestsStore.getExistingRequestWithRequestId(msg.requestId) - ?: return Unit.also { - Timber.tag(loggerTag.value) - .v("[${myUserId.take(8)}]: Can't start unknown request ${msg.requestId}") - msg.deferred.completeExceptionally(java.lang.IllegalArgumentException("Unknown request")) - } - - if (matchingRequest.state != EVerificationState.Ready) { - Timber.tag(loggerTag.value) - .v("[${myUserId.take(8)}]: Can't start a non ready request ${msg.requestId}") - msg.deferred.completeExceptionally(java.lang.IllegalStateException("Can't start a non ready request")) - return - } - - val otherDeviceId = matchingRequest.otherDeviceId() ?: return Unit.also { - Timber.tag(loggerTag.value) - .v("[${myUserId.take(8)}]: Can't start null other device id ${msg.requestId}") - msg.deferred.completeExceptionally(java.lang.IllegalArgumentException("Failed to find other device Id")) - } - - val existingTransaction = getExistingTransaction(msg.otherUserId, msg.requestId) - if (existingTransaction is SasVerificationTransaction) { - // there is already an existing transaction?? - Timber.tag(loggerTag.value) - .v("[${myUserId.take(8)}]: Can't start, already started ${msg.requestId}") - msg.deferred.completeExceptionally(IllegalStateException("Already started")) - return - } - val startMessage = KotlinSasTransaction.sasStart( - inRoom = matchingRequest.roomId != null, - fromDevice = verificationTrustBackend.getMyDeviceId(), - requestId = msg.requestId - ) - - Timber.tag(loggerTag.value) - .v("[${myUserId.take(8)}]:sending start to other ${msg.requestId} in room ${matchingRequest.roomId}") - transportLayer.sendToOther( - matchingRequest, - EventType.KEY_VERIFICATION_START, - startMessage, - ) - - Timber.tag(loggerTag.value) - .v("[${myUserId.take(8)}]: start sent to other ${msg.requestId}") - - // should check if already one (and cancel it) - val tx = KotlinSasTransaction( - channel = channel, - transactionId = msg.requestId, - state = SasTransactionState.SasStarted, - otherUserId = msg.otherUserId, - myUserId = myUserId, - myTrustedMSK = verificationTrustBackend.getMyTrustedMasterKeyBase64(), - otherDeviceId = otherDeviceId, - myDeviceId = verificationTrustBackend.getMyDeviceId(), - myDeviceFingerprint = verificationTrustBackend.getMyDevice().fingerprint().orEmpty(), - startReq = startMessage.asValidObject() as ValidVerificationInfoStart.SasVerificationInfoStart, - isIncoming = false, - isToDevice = matchingRequest.roomId == null, - olmSAS = olmPrimitiveProvider.provideOlmSas() - ) - - matchingRequest.state = EVerificationState.WeStarted - dispatchUpdate(VerificationEvent.RequestUpdated(matchingRequest.toPendingVerificationRequest())) - addTransaction(tx) - - msg.deferred.complete(tx) - } - - private suspend fun handleActionReciprocateQR(msg: VerificationIntent.ActionReciprocateQrVerification) { - Timber.tag(loggerTag.value) - .d("[${myUserId.take(8)}] handle reciprocate for ${msg.requestId}") - val matchingRequest = verificationRequestsStore.getExistingRequestWithRequestId(msg.requestId) - ?: return Unit.also { - Timber.tag(loggerTag.value) - .d("[${myUserId.take(8)}] No matching request, abort ${msg.requestId}") - msg.deferred.completeExceptionally(java.lang.IllegalArgumentException("Unknown request")) - } - - if (matchingRequest.state != EVerificationState.Ready) { - Timber.tag(loggerTag.value) - .d("[${myUserId.take(8)}] Can't start if not ready, abort ${msg.requestId}") - msg.deferred.completeExceptionally(java.lang.IllegalStateException("Can't start a non ready request")) - return - } - - val otherDeviceId = matchingRequest.otherDeviceId() ?: return Unit.also { - msg.deferred.completeExceptionally(java.lang.IllegalArgumentException("Failed to find other device Id")) - } - - val existingTransaction = getExistingTransaction(msg.otherUserId, msg.requestId) - // what if there is an existing?? - if (existingTransaction != null) { - // cancel or replace?? - Timber.tag(loggerTag.value) - .w("[${myUserId.take(8)}] There is already a started transaction for request ${msg.requestId}") - return - } - - val myMasterKey = verificationTrustBackend.getUserMasterKeyBase64(myUserId) - - // Check the other device view of my MSK - val otherQrCodeData = msg.scannedData.toQrCodeData() - when (otherQrCodeData) { - null -> { - Timber.tag(loggerTag.value) - .d("[${myUserId.take(8)}] Malformed QR code ${msg.requestId}") - msg.deferred.completeExceptionally(IllegalArgumentException("Malformed QrCode data")) - return - } - is QrCodeData.VerifyingAnotherUser -> { - // key2 (aka otherUserMasterCrossSigningPublicKey) is what the one displaying the QR code (other user) think my MSK is. - // Let's check that it's correct - // If not -> Cancel - val whatOtherThinksMyMskIs = otherQrCodeData.otherUserMasterCrossSigningPublicKey - if (whatOtherThinksMyMskIs != myMasterKey) { - Timber.tag(loggerTag.value) - .d("[${myUserId.take(8)}] ## Verification QR: Invalid other master key ${otherQrCodeData.otherUserMasterCrossSigningPublicKey}") - cancelRequest(matchingRequest, CancelCode.MismatchedKeys) - msg.deferred.complete(null) - return - } - - val whatIThinkOtherMskIs = verificationTrustBackend.getUserMasterKeyBase64(matchingRequest.otherUserId) - if (whatIThinkOtherMskIs != otherQrCodeData.userMasterCrossSigningPublicKey) { - Timber.tag(loggerTag.value) - .d("[${myUserId.take(8)}] ## Verification QR: Invalid other master key ${otherQrCodeData.otherUserMasterCrossSigningPublicKey}") - cancelRequest(matchingRequest, CancelCode.MismatchedKeys) - msg.deferred.complete(null) - return - } - } - is QrCodeData.SelfVerifyingMasterKeyTrusted -> { - if (matchingRequest.otherUserId != myUserId) { - Timber.tag(loggerTag.value) - .d("[${myUserId.take(8)}] Self mode qr with wrong user ${matchingRequest.otherUserId}") - cancelRequest(matchingRequest, CancelCode.MismatchedUser) - msg.deferred.complete(null) - return - } - // key1 (aka userMasterCrossSigningPublicKey) is the session displaying the QR code view of our MSK. - // Let's check that I see the same MSK - // If not -> Cancel - val whatOtherThinksOurMskIs = otherQrCodeData.userMasterCrossSigningPublicKey - if (whatOtherThinksOurMskIs != myMasterKey) { - Timber.tag(loggerTag.value) - .d("[${myUserId.take(8)}] ## Verification QR: Invalid other master key ${otherQrCodeData.userMasterCrossSigningPublicKey}") - cancelRequest(matchingRequest, CancelCode.MismatchedKeys) - msg.deferred.complete(null) - return - } - val whatOtherThinkMyDeviceKeyIs = otherQrCodeData.otherDeviceKey - val myDeviceKey = verificationTrustBackend.getMyDevice().fingerprint() - if (whatOtherThinkMyDeviceKeyIs != myDeviceKey) { - Timber.tag(loggerTag.value) - .d("[${myUserId.take(8)}] ## Verification QR: Invalid other device key ${otherQrCodeData.userMasterCrossSigningPublicKey}") - cancelRequest(matchingRequest, CancelCode.MismatchedKeys) - msg.deferred.complete(null) - return - } - } - is QrCodeData.SelfVerifyingMasterKeyNotTrusted -> { - if (matchingRequest.otherUserId != myUserId) { - Timber.tag(loggerTag.value) - .d("[${myUserId.take(8)}] Self mode qr with wrong user ${matchingRequest.otherUserId}") - cancelRequest(matchingRequest, CancelCode.MismatchedUser) - msg.deferred.complete(null) - return - } - // key2 (aka userMasterCrossSigningPublicKey) is the session displaying the QR code view of our MSK. - // Let's check that it's the good one - // If not -> Cancel - val otherDeclaredDeviceKey = otherQrCodeData.deviceKey - val whatIThinkItIs = verificationTrustBackend.getUserDevice(matchingRequest.otherUserId, otherDeviceId)?.fingerprint() - - if (otherDeclaredDeviceKey != whatIThinkItIs) { - Timber.tag(loggerTag.value) - .d("[${myUserId.take(8)}] ## Verification QR: Invalid other device key $otherDeviceId") - cancelRequest(matchingRequest, CancelCode.MismatchedKeys) - msg.deferred.complete(null) - } - - val ownMasterKeyTrustedAsSeenByOther = otherQrCodeData.userMasterCrossSigningPublicKey - if (ownMasterKeyTrustedAsSeenByOther != myMasterKey) { - Timber.tag(loggerTag.value) - .d("[${myUserId.take(8)}] ## Verification QR: Invalid other master key ${otherQrCodeData.userMasterCrossSigningPublicKey}") - cancelRequest(matchingRequest, CancelCode.MismatchedKeys) - msg.deferred.complete(null) - return - } - } - } - - // All checks are correct - // Send the shared secret so that sender can trust me - // qrCodeData.sharedSecret will be used to send the start request - val message = if (matchingRequest.roomId != null) { - MessageVerificationStartContent( - fromDevice = verificationTrustBackend.getMyDeviceId(), - hashes = null, - keyAgreementProtocols = null, - messageAuthenticationCodes = null, - shortAuthenticationStrings = null, - method = VERIFICATION_METHOD_RECIPROCATE, - relatesTo = RelationDefaultContent( - type = RelationType.REFERENCE, - eventId = msg.requestId - ), - sharedSecret = otherQrCodeData.sharedSecret - ) - } else { - KeyVerificationStart( - fromDevice = verificationTrustBackend.getMyDeviceId(), - sharedSecret = otherQrCodeData.sharedSecret, - method = VERIFICATION_METHOD_RECIPROCATE, - ) - } - - try { - transportLayer.sendToOther(matchingRequest, EventType.KEY_VERIFICATION_START, message) - } catch (failure: Throwable) { - Timber.tag(loggerTag.value) - .d("[${myUserId.take(8)}] Failed to send reciprocate message") - msg.deferred.completeExceptionally(failure) - return - } - - matchingRequest.state = EVerificationState.WeStarted - dispatchUpdate(VerificationEvent.RequestUpdated(matchingRequest.toPendingVerificationRequest())) - - val tx = KotlinQRVerification( - channel = this.channel, - state = QRCodeVerificationState.Reciprocated, - qrCodeData = msg.scannedData.toQrCodeData(), - method = VerificationMethod.QR_CODE_SCAN, - transactionId = msg.requestId, - otherUserId = msg.otherUserId, - otherDeviceId = matchingRequest.otherDeviceId(), - isIncoming = false, - isToDevice = matchingRequest.roomId == null - ) - - addTransaction(tx) - msg.deferred.complete(tx) - } - - private suspend fun handleActionQRScanConfirmed(matchingRequest: KotlinVerificationRequest) { - val transaction = getExistingTransaction(matchingRequest.otherUserId, matchingRequest.requestId) - if (transaction == null) { - // return - Timber.tag(loggerTag.value) - .d("[${myUserId.take(8)}]: No matching transaction for key tId:${matchingRequest.requestId}") - return - } - - if (transaction.state() == QRCodeVerificationState.WaitingForScanConfirmation) { - completeValidQRTransaction(transaction, matchingRequest) - } else { - Timber.tag(loggerTag.value) - .d("[${myUserId.take(8)}]: Unexpected confirm in state tId:${matchingRequest.requestId}") - // TODO throw? - cancelRequest(matchingRequest, CancelCode.MismatchedKeys) - return - } - } - - private suspend fun handleReceiveKey(matchingRequest: KotlinVerificationRequest, msg: VerificationIntent.OnKeyReceived) { - val requestId = msg.validKey.transactionId - - val existing: KotlinSasTransaction = getExistingTransaction(msg.fromUser, requestId) - ?: return Unit.also { - Timber.tag(loggerTag.value) - .v("[${myUserId.take(8)}]: No matching transaction for key tId:$requestId") - } - - // Existing should be in SAS key sent - val isCorrectState = if (existing.isIncoming) { - existing.state == SasTransactionState.SasAccepted - } else { - existing.state == SasTransactionState.SasKeySent - } - - if (!isCorrectState) { - // it's a wrong state should cancel? - Timber.tag(loggerTag.value) - .v("[${myUserId.take(8)}]: Unexpected key in state ${existing.state} for tId:$requestId") - cancelRequest(matchingRequest, CancelCode.UnexpectedMessage) - } - - val otherKey = msg.validKey.key - if (existing.isIncoming) { - // ok i can now send my key and compute the sas code - val pubKey = existing.olmSAS.publicKey - val keyMessage = KotlinSasTransaction.sasKeyMessage(matchingRequest.roomId != null, requestId, pubKey) - try { - transportLayer.sendToOther( - matchingRequest, - EventType.KEY_VERIFICATION_KEY, - keyMessage, - ) - if (BuildConfig.LOG_PRIVATE_DATA) { - Timber.tag(loggerTag.value) - .v("[${myUserId.take(8)}]:i calculate SAS my key $pubKey their Key: $otherKey") - } - existing.calculateSASBytes(otherKey) - existing.state = SasTransactionState.SasShortCodeReady - if (BuildConfig.LOG_PRIVATE_DATA) { - Timber.tag(loggerTag.value) - .v("[${myUserId.take(8)}]:i CODE ${existing.getDecimalCodeRepresentation()}") - Timber.tag(loggerTag.value) - .v("[${myUserId.take(8)}]:i EMOJI CODE ${existing.getEmojiCodeRepresentation().joinToString(" ") { it.emoji }}") - } - dispatchUpdate(VerificationEvent.TransactionUpdated(existing)) - } catch (failure: Throwable) { - existing.state = SasTransactionState.Cancelled(CancelCode.UserError, true) - matchingRequest.state = EVerificationState.Cancelled - matchingRequest.cancelCode = CancelCode.UserError - dispatchUpdate(VerificationEvent.TransactionUpdated(existing)) - dispatchUpdate(VerificationEvent.RequestUpdated(matchingRequest.toPendingVerificationRequest())) - return - } - } else { - // Upon receipt of the m.key.verification.key message from Bob’s device, - // Alice’s device checks that the commitment property from the Bob’s m.key.verification.accept - // message is the same as the expected value based on the value of the key property received - // in Bob’s m.key.verification.key and the content of Alice’s m.key.verification.start message. - - // check commitment - val concat = otherKey + existing.startReq!!.canonicalJson - - val otherCommitment = try { - hashUsingAgreedHashMethod(existing.accepted?.hash, concat) - } catch (failure: Throwable) { - Timber.tag(loggerTag.value) - .v(failure, "[${myUserId.take(8)}]: Failed to compute hash for tId:$requestId") - cancelRequest(matchingRequest, CancelCode.InvalidMessage) - } - - if (otherCommitment == existing.accepted?.commitment) { - if (BuildConfig.LOG_PRIVATE_DATA) { - Timber.tag(loggerTag.value) - .v("[${myUserId.take(8)}]:o calculate SAS my key ${existing.olmSAS.publicKey} their Key: $otherKey") - } - existing.calculateSASBytes(otherKey) - existing.state = SasTransactionState.SasShortCodeReady - dispatchUpdate(VerificationEvent.TransactionUpdated(existing)) - if (BuildConfig.LOG_PRIVATE_DATA) { - Timber.tag(loggerTag.value) - .v("[${myUserId.take(8)}]:o CODE ${existing.getDecimalCodeRepresentation()}") - Timber.tag(loggerTag.value) - .v("[${myUserId.take(8)}]:o EMOJI CODE ${existing.getEmojiCodeRepresentation().joinToString(" ") { it.emoji }}") - } - } else { - // bad commitment - Timber.tag(loggerTag.value) - .v("[${myUserId.take(8)}]: Bad Commitment for tId:$requestId actual:$otherCommitment ") - cancelRequest(matchingRequest, CancelCode.MismatchedCommitment) - return - } - } - } - - private suspend fun handleMacReceived(matchingRequest: KotlinVerificationRequest, msg: VerificationIntent.OnMacReceived) { - val requestId = msg.validMac.transactionId - - val existing: KotlinSasTransaction = getExistingTransaction(msg.fromUser, requestId) - ?: return Unit.also { - Timber.tag(loggerTag.value) - .v("[${myUserId.take(8)}] on Mac for unknown transaction with id:$requestId") - } - - when (existing.state) { - is SasTransactionState.SasMacSent -> { - existing.theirMac = msg.validMac - finalizeSasTransaction(existing, msg.validMac, matchingRequest, existing.transactionId) - } - is SasTransactionState.SasShortCodeReady -> { - // I can start verify, store it - existing.theirMac = msg.validMac - existing.state = SasTransactionState.SasMacReceived(false) - dispatchUpdate(VerificationEvent.TransactionUpdated(existing)) - } - else -> { - // it's a wrong state should cancel? - Timber.tag(loggerTag.value) - .v("[${myUserId.take(8)}] on Mac in unexpected state ${existing.state} id:$requestId") - cancelRequest(matchingRequest, CancelCode.UnexpectedMessage) - } - } - } - - private suspend fun handleSasCodeDoesNotMatch(msg: VerificationIntent.ActionSASCodeDoesNotMatch) { - val transactionId = msg.transactionId - val matchingRequest = verificationRequestsStore.getExistingRequestWithRequestId(msg.transactionId) - ?: return Unit.also { - msg.deferred.completeExceptionally(IllegalStateException("Unknown Request")) - } - if (matchingRequest.isFinished()) { - return Unit.also { - msg.deferred.completeExceptionally(IllegalStateException("Request was cancelled")) - } - } - val existing: KotlinSasTransaction = getExistingTransaction(transactionId) - ?: return Unit.also { - msg.deferred.completeExceptionally(IllegalStateException("Unknown Transaction")) - } - - val isCorrectState = when (val state = existing.state) { - is SasTransactionState.SasShortCodeReady -> true - is SasTransactionState.SasMacReceived -> !state.codeConfirmed - else -> false - } - if (!isCorrectState) { - return Unit.also { - msg.deferred.completeExceptionally(IllegalStateException("Unexpected action, can't match in this state")) - } - } - try { - cancelRequest(matchingRequest, CancelCode.MismatchedSas) - msg.deferred.complete(Unit) - } catch (failure: Throwable) { - msg.deferred.completeExceptionally(failure) - } - } - - private suspend fun handleDoneReceived(matchingRequest: KotlinVerificationRequest, msg: VerificationIntent.OnDoneReceived) { - val requestId = msg.transactionId - - val existing: VerificationTransaction = getExistingTransaction(msg.fromUser, requestId) - ?: return Unit.also { - Timber.v("on accept received in room ${msg.viaRoom} for verification id:${requestId} in room ${matchingRequest.roomId}") - } - - when { - existing is KotlinSasTransaction -> { - val state = existing.state - val isCorrectState = state is SasTransactionState.Done && !state.otherDone - - if (isCorrectState) { - existing.state = SasTransactionState.Done(true) - dispatchUpdate(VerificationEvent.TransactionUpdated(existing)) - // we can forget about it - verificationRequestsStore.deleteTransaction(matchingRequest.otherUserId, matchingRequest.requestId) - // XXX whatabout waiting for done? - matchingRequest.state = EVerificationState.Done - dispatchUpdate(VerificationEvent.RequestUpdated(matchingRequest.toPendingVerificationRequest())) - } else { - // TODO cancel? - Timber.tag(loggerTag.value) - .d("[${myUserId.take(8)}]: Unexpected done in state $state") - - cancelRequest(matchingRequest, CancelCode.UnexpectedMessage) - } - } - existing is KotlinQRVerification -> { - val state = existing.state() - when (state) { - QRCodeVerificationState.Reciprocated -> { - completeValidQRTransaction(existing, matchingRequest) - } - QRCodeVerificationState.WaitingForOtherDone -> { - matchingRequest.state = EVerificationState.Done - dispatchUpdate(VerificationEvent.RequestUpdated(matchingRequest.toPendingVerificationRequest())) - } - else -> { - Timber.tag(loggerTag.value) - .d("[${myUserId.take(8)}]: Unexpected done in state $state") - cancelRequest(matchingRequest, CancelCode.UnexpectedMessage) - } - } - } - else -> { - // unexpected message? - cancelRequest(matchingRequest, CancelCode.UnexpectedMessage) - } - } - } - - private suspend fun completeValidQRTransaction(existing: KotlinQRVerification, matchingRequest: KotlinVerificationRequest) { - var shouldRequestSecret = false - // Ok so the other side is fine let's trust what we need to trust - when (existing.qrCodeData) { - is QrCodeData.VerifyingAnotherUser -> { - // let's trust him - // it's his code scanned so user is him and other me - try { - verificationTrustBackend.trustUser(matchingRequest.otherUserId) - } catch (failure: Throwable) { - // fail silently? - // at least it will be marked as trusted locally? - } - } - is QrCodeData.SelfVerifyingMasterKeyNotTrusted -> { - // the other device is the one that doesn't trust yet our MSK - // As all is good I can upload a signature for my new device - - // Also notify the secret share manager for the soon to come secret share requests - secretShareManager.onVerificationCompleteForDevice(matchingRequest.otherDeviceId()!!) - try { - verificationTrustBackend.trustOwnDevice(matchingRequest.otherDeviceId()!!) - } catch (failure: Throwable) { - // network problem?? - Timber.w("## Verification: Failed to sign new device ${matchingRequest.otherDeviceId()}, ${failure.localizedMessage}") - } - } - is QrCodeData.SelfVerifyingMasterKeyTrusted -> { - // I can trust my MSK - verificationTrustBackend.markMyMasterKeyAsTrusted() - shouldRequestSecret = true - } - null -> { - // This shouldn't happen? cancel? - } - } - - transportLayer.sendToOther( - matchingRequest, - EventType.KEY_VERIFICATION_DONE, - if (matchingRequest.roomId != null) { - MessageVerificationDoneContent( - relatesTo = RelationDefaultContent( - RelationType.REFERENCE, - matchingRequest.requestId - ) - ) - } else { - KeyVerificationDone(matchingRequest.requestId) - } - ) - - existing.state = QRCodeVerificationState.Done - dispatchUpdate(VerificationEvent.TransactionUpdated(existing)) - // we can forget about it - verificationRequestsStore.deleteTransaction(matchingRequest.otherUserId, matchingRequest.requestId) - matchingRequest.state = EVerificationState.WaitingForDone - dispatchUpdate(VerificationEvent.RequestUpdated(matchingRequest.toPendingVerificationRequest())) - - if (shouldRequestSecret) { - matchingRequest.otherDeviceId()?.let { otherDeviceId -> - secretShareManager.requestSecretTo(otherDeviceId, MASTER_KEY_SSSS_NAME) - secretShareManager.requestSecretTo(otherDeviceId, SELF_SIGNING_KEY_SSSS_NAME) - secretShareManager.requestSecretTo(otherDeviceId, USER_SIGNING_KEY_SSSS_NAME) - secretShareManager.requestSecretTo(otherDeviceId, KEYBACKUP_SECRET_SSSS_NAME) - } - } - } - - private suspend fun handleSasCodeMatch(msg: VerificationIntent.ActionSASCodeMatches) { - val transactionId = msg.transactionId - val matchingRequest = verificationRequestsStore.getExistingRequestWithRequestId(msg.transactionId) - ?: return Unit.also { - msg.deferred.completeExceptionally(IllegalStateException("Unknown Request")) - } - - if (matchingRequest.state != EVerificationState.WeStarted && - matchingRequest.state != EVerificationState.Started) { - return Unit.also { - msg.deferred.completeExceptionally(IllegalStateException("Can't accept code in state: ${matchingRequest.state}")) - } - } - - val existing: KotlinSasTransaction = getExistingTransaction(transactionId) - ?: return Unit.also { - msg.deferred.completeExceptionally(IllegalStateException("Unknown Transaction")) - } - - val isCorrectState = when (val state = existing.state) { - is SasTransactionState.SasShortCodeReady -> true - is SasTransactionState.SasMacReceived -> !state.codeConfirmed - else -> false - } - if (!isCorrectState) { - return Unit.also { - msg.deferred.completeExceptionally(IllegalStateException("Unexpected action, can't match in this state")) - } - } - - val macInfo = existing.computeMyMac() - - val macMsg = KotlinSasTransaction.sasMacMessage(matchingRequest.roomId != null, transactionId, macInfo) - try { - transportLayer.sendToOther(matchingRequest, EventType.KEY_VERIFICATION_MAC, macMsg) - } catch (failure: Throwable) { - // it's a network problem, we don't need to cancel, user can retry? - msg.deferred.completeExceptionally(failure) - return - } - - // Do I already have their Mac? - val theirMac = existing.theirMac - if (theirMac != null) { - finalizeSasTransaction(existing, theirMac, matchingRequest, transactionId) - } else { - existing.state = SasTransactionState.SasMacSent - dispatchUpdate(VerificationEvent.TransactionUpdated(existing)) - } - - msg.deferred.complete(Unit) - } - - private suspend fun finalizeSasTransaction( - existing: KotlinSasTransaction, - theirMac: ValidVerificationInfoMac, - matchingRequest: KotlinVerificationRequest, - transactionId: String - ) { - val result = existing.verifyMacs( - theirMac, - verificationTrustBackend.getUserDeviceList(matchingRequest.otherUserId), - verificationTrustBackend.getUserMasterKeyBase64(matchingRequest.otherUserId) - ) - - Timber.tag(loggerTag.value) - .v("[${myUserId.take(8)}] verify macs result $result id:$transactionId") - when (result) { - is KotlinSasTransaction.MacVerificationResult.Success -> { - // mark the devices as locally trusted - result.verifiedDeviceId.forEach { deviceId -> - - verificationTrustBackend.locallyTrustDevice(matchingRequest.otherUserId, deviceId) - - if (matchingRequest.otherUserId == myUserId && verificationTrustBackend.canCrossSign()) { - // If me it's reasonable to sign and upload the device signature for the other part - try { - verificationTrustBackend.trustOwnDevice(deviceId) - } catch (failure: Throwable) { - // network problem?? - Timber.w("## Verification: Failed to sign new device $deviceId, ${failure.localizedMessage}") - } - } - } - - if (result.otherMskTrusted) { - if (matchingRequest.otherUserId == myUserId) { - verificationTrustBackend.markMyMasterKeyAsTrusted() - } else { - // what should we do if this fails :/ - if (verificationTrustBackend.canCrossSign()) { - verificationTrustBackend.trustUser(matchingRequest.otherUserId) - } - } - } - - // we should send done and wait for done - transportLayer.sendToOther( - matchingRequest, - EventType.KEY_VERIFICATION_DONE, - if (matchingRequest.roomId != null) { - MessageVerificationDoneContent( - relatesTo = RelationDefaultContent( - RelationType.REFERENCE, - transactionId - ) - ) - } else { - KeyVerificationDone(transactionId) - } - ) - - existing.state = SasTransactionState.Done(false) - dispatchUpdate(VerificationEvent.TransactionUpdated(existing)) - verificationRequestsStore.rememberPastSuccessfulTransaction(existing) - verificationRequestsStore.deleteTransaction(matchingRequest.otherUserId, transactionId) - matchingRequest.state = EVerificationState.WaitingForDone - dispatchUpdate(VerificationEvent.RequestUpdated(matchingRequest.toPendingVerificationRequest())) - } - KotlinSasTransaction.MacVerificationResult.MismatchKeys, - KotlinSasTransaction.MacVerificationResult.MismatchMacCrossSigning, - is KotlinSasTransaction.MacVerificationResult.MismatchMacDevice, - KotlinSasTransaction.MacVerificationResult.NoDevicesVerified -> { - cancelRequest(matchingRequest, CancelCode.MismatchedKeys) - } - } - } - - private suspend fun handleActionReadyRequest(msg: VerificationIntent.ActionReadyRequest) { - val existing = verificationRequestsStore.getExistingRequestWithRequestId(msg.transactionId) - ?: return Unit.also { - Timber.tag(loggerTag.value).v("Request ${msg.transactionId} not found!") - msg.deferred.complete(null) - } - - if (existing.state != EVerificationState.Requested) { - Timber.tag(loggerTag.value).v("Request ${msg.transactionId} unexpected ready action") - msg.deferred.completeExceptionally(IllegalStateException("Can't ready request in state ${existing.state}")) - return - } - - val otherUserMethods = existing.requestInfo?.methods.orEmpty() - val commonMethods = getMethodAgreement( - otherUserMethods, - msg.methods - ) - if (commonMethods.isEmpty()) { - Timber.tag(loggerTag.value).v("Request ${msg.transactionId} no common methods") - // Upon receipt of Alice’s m.key.verification.request message, if Bob’s device does not understand any of the methods, - // it should not cancel the request as one of his other devices may support the request. - - // Instead, Bob’s device should tell Bob that no supported method was found, and allow him to manually reject the request. - msg.deferred.completeExceptionally(IllegalStateException("Cannot understand any of the methods")) - return - } - - Timber.tag(loggerTag.value) - .v("[${myUserId.take(8)}] Request ${msg.transactionId} agreement is $commonMethods") - - val qrCodeData = if (otherUserMethods.canScanCode() && msg.methods.contains(VerificationMethod.QR_CODE_SHOW)) { - createQrCodeData(msg.transactionId, existing.otherUserId, existing.requestInfo?.fromDevice) - } else { - null - } - - Timber.tag(loggerTag.value) - .v("[${myUserId.take(8)}] Request ${msg.transactionId} code is $qrCodeData") - - val readyInfo = ValidVerificationInfoReady( - msg.transactionId, - verificationTrustBackend.getMyDeviceId(), - commonMethods - ) - - val message = KotlinSasTransaction.sasReady( - inRoom = existing.roomId != null, - requestId = msg.transactionId, - methods = commonMethods, - fromDevice = verificationTrustBackend.getMyDeviceId() - ) - - Timber.tag(loggerTag.value) - .v("[${myUserId.take(8)}] Request ${msg.transactionId} sending ready") - try { - transportLayer.sendToOther(existing, EventType.KEY_VERIFICATION_READY, message) - } catch (failure: Throwable) { - Timber.tag(loggerTag.value) - .v("[${myUserId.take(8)}] Request ${msg.transactionId} failed to send ready") - msg.deferred.completeExceptionally(failure) - return - } - - existing.readyInfo = readyInfo - existing.qrCodeData = qrCodeData - existing.state = EVerificationState.Ready - - // We want to try emit, if not this will suspend until someone consume the flow - dispatchUpdate(VerificationEvent.RequestUpdated(existing.toPendingVerificationRequest())) - - Timber.tag(loggerTag.value).v("Request ${msg.transactionId} updated $existing") - msg.deferred.complete(existing.toPendingVerificationRequest()) - } - - private suspend fun createQrCodeData(requestId: String, otherUserId: String, otherDeviceId: String?): QrCodeData? { - return when { - myUserId != otherUserId -> - createQrCodeDataForDistinctUser(requestId, otherUserId) - verificationTrustBackend.getMyTrustedMasterKeyBase64() != null -> - // This is a self verification and I am the old device (Osborne2) - createQrCodeDataForVerifiedDevice(requestId, otherUserId, otherDeviceId) - else -> - // This is a self verification and I am the new device (Dynabook) - createQrCodeDataForUnVerifiedDevice(requestId) - } - } - - private fun getMethodAgreement( - otherUserMethods: List?, - myMethods: List, - ): List { - if (otherUserMethods.isNullOrEmpty()) { - return emptyList() - } - - val result = mutableSetOf() - - if (VERIFICATION_METHOD_SAS in otherUserMethods && VerificationMethod.SAS in myMethods) { - // Other can do SAS and so do I - result.add(VERIFICATION_METHOD_SAS) - } - - if (VERIFICATION_METHOD_RECIPROCATE in otherUserMethods) { - if (VERIFICATION_METHOD_QR_CODE_SCAN in otherUserMethods && VerificationMethod.QR_CODE_SHOW in myMethods) { - // Other can Scan and I can show QR code - result.add(VERIFICATION_METHOD_QR_CODE_SHOW) - result.add(VERIFICATION_METHOD_RECIPROCATE) - } - if (VERIFICATION_METHOD_QR_CODE_SHOW in otherUserMethods && VerificationMethod.QR_CODE_SCAN in myMethods) { - // Other can show and I can scan QR code - result.add(VERIFICATION_METHOD_QR_CODE_SCAN) - result.add(VERIFICATION_METHOD_RECIPROCATE) - } - } - - return result.toList() - } - - private fun List.canScanCode(): Boolean { - return contains(VERIFICATION_METHOD_QR_CODE_SCAN) && contains(VERIFICATION_METHOD_RECIPROCATE) - } - - private fun List.canShowCode(): Boolean { - return contains(VERIFICATION_METHOD_QR_CODE_SHOW) && contains(VERIFICATION_METHOD_RECIPROCATE) - } - - private suspend fun handleActionRequestVerification(msg: VerificationIntent.ActionRequestVerification) { - val requestsForUser = verificationRequestsStore.getExistingRequestsForUser(msg.otherUserId) - // there can only be one active request per user, so cancel existing ones - requestsForUser.toList().forEach { existingRequest -> - if (!existingRequest.isFinished()) { - Timber.d("## SAS, cancelling pending requests to start a new one") - cancelRequest(existingRequest, CancelCode.User) - } - } - - // XXX We should probably throw here if you try to verify someone else from an untrusted session - val shouldShowQROption = if (msg.otherUserId == myUserId) { - true - } else { - // It's verifying someone else, I should trust my key before doing it? - verificationTrustBackend.getUserMasterKeyBase64(myUserId) != null - } - val methodValues = if (shouldShowQROption) { - // Add reciprocate method if application declares it can scan or show QR codes - // Not sure if it ok to do that (?) - val reciprocateMethod = msg.methods - .firstOrNull { it == VerificationMethod.QR_CODE_SCAN || it == VerificationMethod.QR_CODE_SHOW } - ?.let { listOf(VERIFICATION_METHOD_RECIPROCATE) }.orEmpty() - msg.methods.map { it.toValue() } + reciprocateMethod - } else { - // Filter out SCAN and SHOW qr code method - msg.methods - .filter { it != VerificationMethod.QR_CODE_SHOW && it != VerificationMethod.QR_CODE_SCAN } - .map { it.toValue() } - } - .distinct() - - val validInfo = ValidVerificationInfoRequest( - transactionId = "", - fromDevice = verificationTrustBackend.getMyDeviceId(), - methods = methodValues, - timestamp = clock.epochMillis() - ) - - try { - if (msg.roomId != null) { - val info = MessageVerificationRequestContent( - body = "$myUserId is requesting to verify your key, but your client does not support in-chat key verification." + - " You will need to use legacy key verification to verify keys.", - fromDevice = validInfo.fromDevice, - toUserId = msg.otherUserId, - timestamp = validInfo.timestamp, - methods = validInfo.methods - ) - val eventId = transportLayer.sendInRoom( - type = EventType.MESSAGE, - roomId = msg.roomId, - content = info.toContent() - ) - val request = KotlinVerificationRequest( - requestId = eventId, - incoming = false, - otherUserId = msg.otherUserId, - state = EVerificationState.WaitingForReady, - ageLocalTs = clock.epochMillis() - ).apply { - roomId = msg.roomId - requestInfo = validInfo.copy(transactionId = eventId) - } - verificationRequestsStore.addRequest(msg.otherUserId, request) - msg.deferred.complete(request.toPendingVerificationRequest()) - dispatchUpdate(VerificationEvent.RequestAdded(request.toPendingVerificationRequest())) - } else { - val requestId = LocalEcho.createLocalEchoId() - transportLayer.sendToDeviceEvent( - messageType = EventType.KEY_VERIFICATION_REQUEST, - toSendToDeviceObject = KeyVerificationRequest( - transactionId = requestId, - fromDevice = verificationTrustBackend.getMyDeviceId(), - methods = validInfo.methods, - timestamp = validInfo.timestamp - ), - otherUserId = msg.otherUserId, - targetDevices = msg.targetDevices.orEmpty() - ) - val request = KotlinVerificationRequest( - requestId = requestId, - incoming = false, - otherUserId = msg.otherUserId, - state = EVerificationState.WaitingForReady, - ageLocalTs = clock.epochMillis(), - ).apply { - targetDevices = msg.targetDevices.orEmpty() - roomId = null - requestInfo = validInfo.copy(transactionId = requestId) - } - verificationRequestsStore.addRequest(msg.otherUserId, request) - msg.deferred.complete(request.toPendingVerificationRequest()) - dispatchUpdate(VerificationEvent.RequestAdded(request.toPendingVerificationRequest())) - } - } catch (failure: Throwable) { - // some network problem - msg.deferred.completeExceptionally(failure) - return - } - } - - private suspend fun handleReadyReceived(msg: VerificationIntent.OnReadyReceived) { - val matchingRequest = verificationRequestsStore.getExistingRequest(msg.fromUser, msg.transactionId) - ?: return Unit.also { - Timber.tag(loggerTag.value) - .v("[${myUserId.take(8)}]: No matching request to ready tId:${msg.transactionId}") -// cancelRequest(msg.transactionId, msg.viaRoom, msg.fromUser, msg.readyInfo.fromDevice, CancelCode.UnknownTransaction) - } - val myDevice = verificationTrustBackend.getMyDeviceId() - - if (matchingRequest.state != EVerificationState.WaitingForReady) { - cancelRequest(matchingRequest, CancelCode.UnexpectedMessage) - return - } - // for room verification (user) - // TODO if room and incoming I should check that right? - // actually it will not reach that point? handleReadyByAnotherOfMySessionReceived would be called instead? and - // the actor never sees event send by me in rooms - if (matchingRequest.otherUserId != myUserId && msg.fromUser == myUserId && msg.readyInfo.fromDevice != myDevice) { - // it's a ready from another of my devices, so we should just - // ignore following messages related to that request - Timber.tag(loggerTag.value) - .v("[${myUserId.take(8)}]: ready from another of my devices, make inactive") - matchingRequest.state = EVerificationState.HandledByOtherSession - dispatchUpdate(VerificationEvent.RequestUpdated(matchingRequest.toPendingVerificationRequest())) - return - } - - if (matchingRequest.requestInfo?.methods?.canShowCode().orFalse() && - msg.readyInfo.methods.canScanCode()) { - matchingRequest.qrCodeData = createQrCodeData(matchingRequest.requestId, msg.fromUser, msg.readyInfo.fromDevice) - } - matchingRequest.readyInfo = msg.readyInfo - matchingRequest.state = EVerificationState.Ready - dispatchUpdate(VerificationEvent.RequestUpdated(matchingRequest.toPendingVerificationRequest())) - -// if (matchingRequest.readyInfo != null) { -// // TODO we already received a ready, cancel? or ignore -// Timber.tag(loggerTag.value) -// .v("[${myUserId.take(8)}]: already received a ready for transaction ${msg.transactionId}") -// return -// } -// -// updatePendingRequest( -// matchingRequest.copy( -// readyInfo = msg.readyInfo, -// ) -// ) - - if (msg.viaRoom == null) { - // we should cancel to others if it was requested via to_device - // via room the other session will see the ready in room an mark the transaction as inactive for them - val deviceIds = verificationTrustBackend.getUserDeviceList(matchingRequest.otherUserId) - .filter { it.deviceId != msg.readyInfo.fromDevice } - // if it's me we don't want to send self cancel - .filter { it.deviceId != myDevice } - .map { it.deviceId } - - try { - transportLayer.sendToDeviceEvent( - EventType.KEY_VERIFICATION_CANCEL, - KeyVerificationCancel( - msg.transactionId, - CancelCode.AcceptedByAnotherDevice.value, - CancelCode.AcceptedByAnotherDevice.humanReadable - ), - matchingRequest.otherUserId, - deviceIds, - ) - } catch (failure: Throwable) { - // just fail silently in this case - Timber.v("Failed to notify that accepted by another device") - } - } - } - - private suspend fun handleReadyByAnotherOfMySessionReceived(msg: VerificationIntent.OnReadyByAnotherOfMySessionReceived) { - val matchingRequest = verificationRequestsStore.getExistingRequest(msg.fromUser, msg.transactionId) - ?: return - - // it's a ready from another of my devices, so we should just - // ignore following messages related to that request - Timber.tag(loggerTag.value) - .v("[${myUserId.take(8)}]: ready from another of my devices, make inactive") - matchingRequest.state = EVerificationState.HandledByOtherSession - dispatchUpdate(VerificationEvent.RequestUpdated(matchingRequest.toPendingVerificationRequest())) - return - } - -// private suspend fun updatePendingRequest(updated: PendingVerificationRequest) { -// val requestsForUser = pendingRequests.getOrPut(updated.otherUserId) { mutableListOf() } -// val index = requestsForUser.indexOfFirst { -// it.transactionId == updated.transactionId || -// it.transactionId == null && it.localId == updated.localId -// } -// if (index != -1) { -// requestsForUser.removeAt(index) -// } -// requestsForUser.add(updated) -// dispatchUpdate(VerificationEvent.RequestUpdated(updated)) -// } - - private fun dispatchRequestAdded(tx: KotlinVerificationRequest) { - Timber.v("## SAS dispatchRequestAdded txId:${tx.requestId}") - dispatchUpdate(VerificationEvent.RequestAdded(tx.toPendingVerificationRequest())) - } - -// Utilities - - private suspend fun createQrCodeDataForDistinctUser(requestId: String, otherUserId: String): QrCodeData.VerifyingAnotherUser? { - val myMasterKey = verificationTrustBackend.getMyTrustedMasterKeyBase64() - ?: run { - Timber.w("## Unable to get my master key") - return null - } - - val otherUserMasterKey = verificationTrustBackend.getUserMasterKeyBase64(otherUserId) - ?: run { - Timber.w("## Unable to get other user master key") - return null - } - - return QrCodeData.VerifyingAnotherUser( - transactionId = requestId, - userMasterCrossSigningPublicKey = myMasterKey, - otherUserMasterCrossSigningPublicKey = otherUserMasterKey, - sharedSecret = generateSharedSecretV2() - ) - } - - // Create a QR code to display on the old device (Osborne2) - private suspend fun createQrCodeDataForVerifiedDevice(requestId: String, otherUserId: String, otherDeviceId: String?): QrCodeData.SelfVerifyingMasterKeyTrusted? { - val myMasterKey = verificationTrustBackend.getUserMasterKeyBase64(myUserId) - ?: run { - Timber.w("## Unable to get my master key") - return null - } - - val otherDeviceKey = otherDeviceId - ?.let { - verificationTrustBackend.getUserDevice(otherUserId, otherDeviceId)?.fingerprint() - } - ?: run { - Timber.w("## Unable to get other device data") - return null - } - - return QrCodeData.SelfVerifyingMasterKeyTrusted( - transactionId = requestId, - userMasterCrossSigningPublicKey = myMasterKey, - otherDeviceKey = otherDeviceKey, - sharedSecret = generateSharedSecretV2() - ) - } - - // Create a QR code to display on the new device (Dynabook) - private suspend fun createQrCodeDataForUnVerifiedDevice(requestId: String): QrCodeData.SelfVerifyingMasterKeyNotTrusted? { - val myMasterKey = verificationTrustBackend.getUserMasterKeyBase64(myUserId) - ?: run { - Timber.w("## Unable to get my master key") - return null - } - - val myDeviceKey = verificationTrustBackend.getUserDevice(myUserId, verificationTrustBackend.getMyDeviceId())?.fingerprint() - ?: return null.also { - Timber.w("## Unable to get my fingerprint") - } - - return QrCodeData.SelfVerifyingMasterKeyNotTrusted( - transactionId = requestId, - deviceKey = myDeviceKey, - userMasterCrossSigningPublicKey = myMasterKey, - sharedSecret = generateSharedSecretV2() - ) - } - - private suspend fun cancelRequest(request: KotlinVerificationRequest, code: CancelCode) { - request.state = EVerificationState.Cancelled - request.cancelCode = code - dispatchUpdate(VerificationEvent.RequestUpdated(request.toPendingVerificationRequest())) - - // should also update SAS/QR transaction - getExistingTransaction(request.otherUserId, request.requestId)?.let { - it.state = SasTransactionState.Cancelled(code, true) - verificationRequestsStore.deleteTransaction(request.otherUserId, request.requestId) - dispatchUpdate(VerificationEvent.TransactionUpdated(it)) - } - getExistingTransaction(request.otherUserId, request.requestId)?.let { - it.state = QRCodeVerificationState.Cancelled - verificationRequestsStore.deleteTransaction(request.otherUserId, request.requestId) - dispatchUpdate(VerificationEvent.TransactionUpdated(it)) - } - - cancelRequest( - request.requestId, - request.roomId, - request.otherUserId, - request.otherDeviceId()?.let { listOf(it) } ?: request.targetDevices ?: emptyList(), - code - ) - } - - private suspend fun cancelRequest(transactionId: String, roomId: String?, otherUserId: String?, otherDeviceIds: List, code: CancelCode) { - try { - if (roomId == null) { - cancelTransactionToDevice( - transactionId, - otherUserId.orEmpty(), - otherDeviceIds, - code - ) - } else { - cancelTransactionInRoom( - roomId, - transactionId, - code - ) - } - } catch (failure: Throwable) { - Timber.w("FAILED to cancel request $transactionId reason:${code.humanReadable}") - // continue anyhow - } - } - - private suspend fun cancelTransactionToDevice(transactionId: String, otherUserId: String, otherUserDeviceIds: List, code: CancelCode) { - Timber.d("## SAS canceling transaction $transactionId for reason $code") - val cancelMessage = KeyVerificationCancel.create(transactionId, code) -// val contentMap = MXUsersDevicesMap() -// contentMap.setObject(otherUserId, otherUserDeviceId, cancelMessage) - transportLayer.sendToDeviceEvent( - messageType = EventType.KEY_VERIFICATION_CANCEL, - toSendToDeviceObject = cancelMessage, - otherUserId = otherUserId, - targetDevices = otherUserDeviceIds - ) -// sendToDeviceTask -// .execute(SendToDeviceTask.Params(EventType.KEY_VERIFICATION_CANCEL, contentMap)) - } - - private suspend fun cancelTransactionInRoom(roomId: String, transactionId: String, code: CancelCode) { - Timber.d("## SAS canceling transaction $transactionId for reason $code") - val cancelMessage = MessageVerificationCancelContent.create(transactionId, code) - transportLayer.sendInRoom( - type = EventType.KEY_VERIFICATION_CANCEL, - roomId = roomId, - content = cancelMessage.toEventContent() - ) - } - - private fun hashUsingAgreedHashMethod(hashMethod: String?, toHash: String): String { - if ("sha256" == hashMethod?.lowercase(Locale.ROOT)) { - return olmPrimitiveProvider.sha256(toHash) - } - throw java.lang.IllegalArgumentException("Unsupported hash method $hashMethod") - } - - private suspend fun addTransaction(tx: VerificationTransaction) { - verificationRequestsStore.addTransaction(tx) - dispatchUpdate(VerificationEvent.TransactionAdded(tx)) - } - - private inline fun getExistingTransaction(otherUserId: String, transactionId: String): T? { - return verificationRequestsStore.getExistingTransaction(otherUserId, transactionId) as? T - } - - private inline fun getExistingTransaction(transactionId: String): T? { - return verificationRequestsStore.getExistingTransaction(transactionId) - .takeIf { it is T } as? T -// txMap.forEach { -// val match = it.value.values -// .firstOrNull { it.transactionId == transactionId } -// ?.takeIf { it is T } -// if (match != null) return match as? T -// } -// return null - } -} diff --git a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/VerificationCryptoPrimitiveProvider.kt b/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/VerificationCryptoPrimitiveProvider.kt deleted file mode 100644 index b0bcbb2e04..0000000000 --- a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/VerificationCryptoPrimitiveProvider.kt +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright 2022 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. - */ - -package org.matrix.android.sdk.internal.crypto.verification - -import org.matrix.android.sdk.internal.crypto.tools.withOlmUtility -import org.matrix.olm.OlmSAS -import javax.inject.Inject - -// Mainly for testing purpose to ease mocking -internal class VerificationCryptoPrimitiveProvider @Inject constructor() { - - fun provideOlmSas(): OlmSAS { - return OlmSAS() - } - - fun sha256(toHash: String): String { - return withOlmUtility { - it.sha256(toHash) - } - } -} diff --git a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/VerificationInfo.kt b/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/VerificationInfo.kt deleted file mode 100644 index 0615773a7b..0000000000 --- a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/VerificationInfo.kt +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright 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. - */ -package org.matrix.android.sdk.internal.crypto.verification - -import org.matrix.android.sdk.api.session.crypto.model.SendToDeviceObject -import org.matrix.android.sdk.api.session.events.model.Content - -internal interface VerificationInfo { - fun toEventContent(): Content? = null - fun toSendToDeviceObject(): SendToDeviceObject? = null - - fun asValidObject(): ValidObjectType? - - /** - * String to identify the transaction. - * This string must be unique for the pair of users performing verification for the duration that the transaction is valid. - * Alice’s device should record this ID and use it in future messages in this transaction. - */ - val transactionId: String? -} diff --git a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/VerificationInfoAccept.kt b/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/VerificationInfoAccept.kt deleted file mode 100644 index 0b9287cb05..0000000000 --- a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/VerificationInfoAccept.kt +++ /dev/null @@ -1,83 +0,0 @@ -/* - * Copyright 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. - */ -package org.matrix.android.sdk.internal.crypto.verification - -internal interface VerificationInfoAccept : VerificationInfo { - /** - * The key agreement protocol that Bob’s device has selected to use, out of the list proposed by Alice’s device. - */ - val keyAgreementProtocol: String? - - /** - * The hash algorithm that Bob’s device has selected to use, out of the list proposed by Alice’s device. - */ - val hash: String? - - /** - * The message authentication code that Bob’s device has selected to use, out of the list proposed by Alice’s device. - */ - val messageAuthenticationCode: String? - - /** - * An array of short authentication string methods that Bob’s client (and Bob) understands. Must be a subset of the list proposed by Alice’s device. - */ - val shortAuthenticationStrings: List? - - /** - * The hash (encoded as unpadded base64) of the concatenation of the device’s ephemeral public key (QB, encoded as unpadded base64) - * and the canonical JSON representation of the m.key.verification.start message. - */ - var commitment: String? - - override fun asValidObject(): ValidVerificationInfoAccept? { - val validTransactionId = transactionId?.takeIf { it.isNotEmpty() } ?: return null - val validKeyAgreementProtocol = keyAgreementProtocol?.takeIf { it.isNotEmpty() } ?: return null - val validHash = hash?.takeIf { it.isNotEmpty() } ?: return null - val validMessageAuthenticationCode = messageAuthenticationCode?.takeIf { it.isNotEmpty() } ?: return null - val validShortAuthenticationStrings = shortAuthenticationStrings?.takeIf { it.isNotEmpty() } ?: return null - val validCommitment = commitment?.takeIf { it.isNotEmpty() } ?: return null - - return ValidVerificationInfoAccept( - validTransactionId, - validKeyAgreementProtocol, - validHash, - validMessageAuthenticationCode, - validShortAuthenticationStrings, - validCommitment - ) - } -} - -internal interface VerificationInfoAcceptFactory { - - fun create( - tid: String, - keyAgreementProtocol: String, - hash: String, - commitment: String, - messageAuthenticationCode: String, - shortAuthenticationStrings: List - ): VerificationInfoAccept -} - -internal data class ValidVerificationInfoAccept( - val transactionId: String, - val keyAgreementProtocol: String, - val hash: String, - val messageAuthenticationCode: String, - val shortAuthenticationStrings: List, - var commitment: String? -) diff --git a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/VerificationInfoCancel.kt b/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/VerificationInfoCancel.kt deleted file mode 100644 index 20e2cdcd33..0000000000 --- a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/VerificationInfoCancel.kt +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright 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. - */ -package org.matrix.android.sdk.internal.crypto.verification - -internal interface VerificationInfoCancel : VerificationInfo { - /** - * machine-readable reason for cancelling, see [CancelCode]. - */ - val code: String? - - /** - * human-readable reason for cancelling. This should only be used if the receiving client does not understand the code given. - */ - val reason: String? - - override fun asValidObject(): ValidVerificationInfoCancel? { - val validTransactionId = transactionId?.takeIf { it.isNotEmpty() } ?: return null - val validCode = code?.takeIf { it.isNotEmpty() } ?: return null - - return ValidVerificationInfoCancel( - validTransactionId, - validCode, - reason - ) - } -} - -internal data class ValidVerificationInfoCancel( - val transactionId: String, - val code: String, - val reason: String? -) diff --git a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/VerificationInfoDone.kt b/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/VerificationInfoDone.kt deleted file mode 100644 index dfbe45a64f..0000000000 --- a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/VerificationInfoDone.kt +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright 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. - */ -package org.matrix.android.sdk.internal.crypto.verification - -import org.matrix.android.sdk.api.session.room.model.message.ValidVerificationDone - -internal interface VerificationInfoDone : VerificationInfo { - - override fun asValidObject(): ValidVerificationDone? { - val validTransactionId = transactionId?.takeIf { it.isNotEmpty() } ?: return null - return ValidVerificationDone(validTransactionId) - } -} diff --git a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/VerificationInfoKey.kt b/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/VerificationInfoKey.kt deleted file mode 100644 index 2885b81a12..0000000000 --- a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/VerificationInfoKey.kt +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright 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. - */ -package org.matrix.android.sdk.internal.crypto.verification - -/** - * Sent by both devices to send their ephemeral Curve25519 public key to the other device. - */ -internal interface VerificationInfoKey : VerificationInfo { - /** - * The device’s ephemeral public key, as an unpadded base64 string. - */ - val key: String? - - override fun asValidObject(): ValidVerificationInfoKey? { - val validTransactionId = transactionId?.takeIf { it.isNotEmpty() } ?: return null - val validKey = key?.takeIf { it.isNotEmpty() } ?: return null - - return ValidVerificationInfoKey( - validTransactionId, - validKey - ) - } -} - -internal interface VerificationInfoKeyFactory { - fun create(tid: String, pubKey: String): VerificationInfoKey -} - -internal data class ValidVerificationInfoKey( - val transactionId: String, - val key: String -) diff --git a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/VerificationInfoMac.kt b/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/VerificationInfoMac.kt deleted file mode 100644 index d6f1d7e4db..0000000000 --- a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/VerificationInfoMac.kt +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright 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. - */ -package org.matrix.android.sdk.internal.crypto.verification - -internal interface VerificationInfoMac : VerificationInfo { - /** - * A map of key ID to the MAC of the key, as an unpadded base64 string, calculated using the MAC key. - */ - val mac: Map? - - /** - * The MAC of the comma-separated, sorted list of key IDs given in the mac property, - * as an unpadded base64 string, calculated using the MAC key. - * For example, if the mac property gives MACs for the keys ed25519:ABCDEFG and ed25519:HIJKLMN, then this property will - * give the MAC of the string “ed25519:ABCDEFG,ed25519:HIJKLMN”. - */ - val keys: String? - - override fun asValidObject(): ValidVerificationInfoMac? { - val validTransactionId = transactionId?.takeIf { it.isNotEmpty() } ?: return null - val validMac = mac?.takeIf { it.isNotEmpty() } ?: return null - val validKeys = keys?.takeIf { it.isNotEmpty() } ?: return null - - return ValidVerificationInfoMac( - validTransactionId, - validMac, - validKeys - ) - } -} - -internal interface VerificationInfoMacFactory { - fun create(tid: String, mac: Map, keys: String): VerificationInfoMac -} - -internal data class ValidVerificationInfoMac( - val transactionId: String, - val mac: Map, - val keys: String -) diff --git a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/VerificationInfoReady.kt b/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/VerificationInfoReady.kt deleted file mode 100644 index d659ed7569..0000000000 --- a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/VerificationInfoReady.kt +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright 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. - */ -package org.matrix.android.sdk.internal.crypto.verification - -import org.matrix.android.sdk.api.session.crypto.verification.ValidVerificationInfoReady -import timber.log.Timber - -/** - * A new event type is added to the key verification framework: m.key.verification.ready, - * which may be sent by the target of the m.key.verification.request message, upon receipt of the m.key.verification.request event. - * - * The m.key.verification.ready event is optional; the recipient of the m.key.verification.request event may respond directly - * with a m.key.verification.start event instead. - */ - -internal interface VerificationInfoReady : VerificationInfo { - /** - * The ID of the device that sent the m.key.verification.ready message. - */ - val fromDevice: String? - - /** - * An array of verification methods that the device supports. - */ - val methods: List? - - override fun asValidObject(): ValidVerificationInfoReady? { - val validTransactionId = transactionId?.takeIf { it.isNotEmpty() } ?: return null.also { - Timber.e("## SAS Invalid room ready content invalid transaction id $transactionId") - } - val validFromDevice = fromDevice?.takeIf { it.isNotEmpty() } ?: return null.also { - Timber.e("## SAS Invalid room ready content invalid fromDevice $fromDevice") - } - val validMethods = methods?.takeIf { it.isNotEmpty() } ?: return null.also { - Timber.e("## SAS Invalid room ready content invalid methods $methods") - } - - return ValidVerificationInfoReady( - validTransactionId, - validFromDevice, - validMethods - ) - } -} - -internal interface MessageVerificationReadyFactory { - fun create(tid: String, methods: List, fromDevice: String): VerificationInfoReady -} diff --git a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/VerificationInfoRequest.kt b/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/VerificationInfoRequest.kt deleted file mode 100644 index 1cf72308b1..0000000000 --- a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/VerificationInfoRequest.kt +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright 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. - */ -package org.matrix.android.sdk.internal.crypto.verification - -import org.matrix.android.sdk.api.session.crypto.verification.ValidVerificationInfoRequest - -internal interface VerificationInfoRequest : VerificationInfo { - - /** - * Required. The device ID which is initiating the request. - */ - val fromDevice: String? - - /** - * Required. The verification methods supported by the sender. - */ - val methods: List? - - /** - * The POSIX timestamp in milliseconds for when the request was made. - * If the request is in the future by more than 5 minutes or more than 10 minutes in the past, - * the message should be ignored by the receiver. - */ - val timestamp: Long? - - override fun asValidObject(): ValidVerificationInfoRequest? { - // FIXME No check on Timestamp? - val validTransactionId = transactionId?.takeIf { it.isNotEmpty() } ?: return null - val validFromDevice = fromDevice?.takeIf { it.isNotEmpty() } ?: return null - val validMethods = methods?.takeIf { it.isNotEmpty() } ?: return null - - return ValidVerificationInfoRequest( - validTransactionId, - validFromDevice, - validMethods, - timestamp - ) - } -} diff --git a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/VerificationInfoStart.kt b/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/VerificationInfoStart.kt deleted file mode 100644 index 46b20a8f97..0000000000 --- a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/VerificationInfoStart.kt +++ /dev/null @@ -1,126 +0,0 @@ -/* - * Copyright 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. - */ -package org.matrix.android.sdk.internal.crypto.verification - -import org.matrix.android.sdk.api.session.crypto.verification.SasMode -import org.matrix.android.sdk.api.session.crypto.verification.SasVerificationTransaction -import org.matrix.android.sdk.internal.crypto.model.rest.VERIFICATION_METHOD_RECIPROCATE -import org.matrix.android.sdk.internal.crypto.model.rest.VERIFICATION_METHOD_SAS - -internal interface VerificationInfoStart : VerificationInfo { - - val method: String? - - /** - * Alice’s device ID. - */ - val fromDevice: String? - - /** - * An array of key agreement protocols that Alice’s client understands. - * Must include “curve25519”. - * Other methods may be defined in the future - */ - val keyAgreementProtocols: List? - - /** - * An array of hashes that Alice’s client understands. - * Must include “sha256”. Other methods may be defined in the future. - */ - val hashes: List? - - /** - * An array of message authentication codes that Alice’s client understands. - * Must include “hkdf-hmac-sha256”. - * Other methods may be defined in the future. - */ - val messageAuthenticationCodes: List? - - /** - * An array of short authentication string methods that Alice’s client (and Alice) understands. - * Must include “decimal”. - * This document also describes the “emoji” method. - * Other methods may be defined in the future - */ - val shortAuthenticationStrings: List? - - /** - * Shared secret, when starting verification with QR code. - */ - val sharedSecret: String? - - fun toCanonicalJson(): String - - override fun asValidObject(): ValidVerificationInfoStart? { - val validTransactionId = transactionId?.takeIf { it.isNotEmpty() } ?: return null - val validFromDevice = fromDevice?.takeIf { it.isNotEmpty() } ?: return null - - return when (method) { - VERIFICATION_METHOD_SAS -> { - val validKeyAgreementProtocols = keyAgreementProtocols?.takeIf { it.isNotEmpty() } ?: return null - val validHashes = hashes?.takeIf { it.contains("sha256") } ?: return null - val validMessageAuthenticationCodes = messageAuthenticationCodes - ?.takeIf { - it.contains(SasVerificationTransaction.SAS_MAC_SHA256) || - it.contains(SasVerificationTransaction.SAS_MAC_SHA256_LONGKDF) - } - ?: return null - val validShortAuthenticationStrings = shortAuthenticationStrings?.takeIf { it.contains(SasMode.DECIMAL) } ?: return null - - ValidVerificationInfoStart.SasVerificationInfoStart( - validTransactionId, - validFromDevice, - validKeyAgreementProtocols, - validHashes, - validMessageAuthenticationCodes, - validShortAuthenticationStrings, - canonicalJson = toCanonicalJson() - ) - } - VERIFICATION_METHOD_RECIPROCATE -> { - val validSharedSecret = sharedSecret?.takeIf { it.isNotEmpty() } ?: return null - - ValidVerificationInfoStart.ReciprocateVerificationInfoStart( - validTransactionId, - validFromDevice, - validSharedSecret - ) - } - else -> null - } - } -} - -internal sealed class ValidVerificationInfoStart( - open val transactionId: String, - open val fromDevice: String -) { - data class SasVerificationInfoStart( - override val transactionId: String, - override val fromDevice: String, - val keyAgreementProtocols: List, - val hashes: List, - val messageAuthenticationCodes: List, - val shortAuthenticationStrings: List, - val canonicalJson: String - ) : ValidVerificationInfoStart(transactionId, fromDevice) - - data class ReciprocateVerificationInfoStart( - override val transactionId: String, - override val fromDevice: String, - val sharedSecret: String - ) : ValidVerificationInfoStart(transactionId, fromDevice) -} diff --git a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/VerificationIntent.kt b/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/VerificationIntent.kt deleted file mode 100644 index d0d88358b9..0000000000 --- a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/VerificationIntent.kt +++ /dev/null @@ -1,162 +0,0 @@ -/* - * Copyright (c) 2022 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. - */ - -package org.matrix.android.sdk.internal.crypto.verification - -import kotlinx.coroutines.CompletableDeferred -import org.matrix.android.sdk.api.session.crypto.verification.PendingVerificationRequest -import org.matrix.android.sdk.api.session.crypto.verification.ValidVerificationInfoReady -import org.matrix.android.sdk.api.session.crypto.verification.ValidVerificationInfoRequest -import org.matrix.android.sdk.api.session.crypto.verification.VerificationMethod -import org.matrix.android.sdk.api.session.crypto.verification.VerificationTransaction - -internal sealed class VerificationIntent { - data class ActionRequestVerification( - val otherUserId: String, - // in case of verification in room - val roomId: String? = null, - val methods: List, - // In case of to device it is sent to a list of devices - val targetDevices: List? = null, - val deferred: CompletableDeferred, - ) : VerificationIntent() - - data class OnVerificationRequestReceived( - val validRequestInfo: ValidVerificationInfoRequest, - val senderId: String, - val roomId: String?, - val timeStamp: Long? = null, -// val deferred: CompletableDeferred, - ) : VerificationIntent() - - data class ActionReadyRequest( - val transactionId: String, - val methods: List, - val deferred: CompletableDeferred - ) : VerificationIntent() - - data class OnReadyReceived( - val transactionId: String, - val fromUser: String, - val viaRoom: String?, - val readyInfo: ValidVerificationInfoReady, - ) : VerificationIntent() - - data class OnReadyByAnotherOfMySessionReceived( - val transactionId: String, - val fromUser: String, - val viaRoom: String?, - ) : VerificationIntent() - - data class GetExistingRequestInRoom( - val transactionId: String, - val roomId: String, - val deferred: CompletableDeferred, - ) : VerificationIntent() - - data class GetExistingRequest( - val transactionId: String, - val otherUserId: String, - val deferred: CompletableDeferred, - ) : VerificationIntent() - - data class GetExistingRequestsForUser( - val userId: String, - val deferred: CompletableDeferred>, - ) : VerificationIntent() - - data class GetExistingTransaction( - val transactionId: String, - val fromUser: String, - val deferred: CompletableDeferred, - ) : VerificationIntent() - - data class ActionStartSasVerification( - val otherUserId: String, - val requestId: String, - val deferred: CompletableDeferred, - ) : VerificationIntent() - - data class ActionReciprocateQrVerification( - val otherUserId: String, - val requestId: String, - val scannedData: String, - val deferred: CompletableDeferred, - ) : VerificationIntent() - - data class ActionConfirmCodeWasScanned( - val otherUserId: String, - val requestId: String, - val deferred: CompletableDeferred, - ) : VerificationIntent() - - data class OnStartReceived( - val viaRoom: String?, - val fromUser: String, - val validVerificationInfoStart: ValidVerificationInfoStart, - ) : VerificationIntent() - - data class OnAcceptReceived( - val viaRoom: String?, - val fromUser: String, - val validAccept: ValidVerificationInfoAccept, - ) : VerificationIntent() - - data class OnKeyReceived( - val viaRoom: String?, - val fromUser: String, - val validKey: ValidVerificationInfoKey, - ) : VerificationIntent() - - data class OnMacReceived( - val viaRoom: String?, - val fromUser: String, - val validMac: ValidVerificationInfoMac, - ) : VerificationIntent() - - data class OnCancelReceived( - val viaRoom: String?, - val fromUser: String, - val validCancel: ValidVerificationInfoCancel, - ) : VerificationIntent() - - data class ActionSASCodeMatches( - val transactionId: String, - val deferred: CompletableDeferred - ) : VerificationIntent() - - data class ActionSASCodeDoesNotMatch( - val transactionId: String, - val deferred: CompletableDeferred - ) : VerificationIntent() - - data class ActionCancel( - val transactionId: String, - val deferred: CompletableDeferred - ) : VerificationIntent() - - data class OnUnableToDecryptVerificationEvent( - val transactionId: String, - val roomId: String, - val fromUser: String, - ) : VerificationIntent() - - data class OnDoneReceived( - val viaRoom: String?, - val fromUser: String, - val transactionId: String, - ) : VerificationIntent() -} diff --git a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/VerificationMessageProcessor.kt b/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/VerificationMessageProcessor.kt deleted file mode 100644 index b15dc60bbf..0000000000 --- a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/VerificationMessageProcessor.kt +++ /dev/null @@ -1,144 +0,0 @@ -/* - * Copyright 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. - */ -package org.matrix.android.sdk.internal.crypto.verification - -import org.matrix.android.sdk.api.session.crypto.verification.VerificationService -import org.matrix.android.sdk.api.session.events.model.Event -import org.matrix.android.sdk.api.session.events.model.EventType -import org.matrix.android.sdk.api.session.events.model.getRelationContent -import org.matrix.android.sdk.api.session.events.model.toModel -import org.matrix.android.sdk.api.session.room.model.message.MessageContent -import org.matrix.android.sdk.api.session.room.model.message.MessageType -import org.matrix.android.sdk.api.session.room.model.message.MessageVerificationReadyContent -import org.matrix.android.sdk.internal.di.DeviceId -import org.matrix.android.sdk.internal.di.UserId -import org.matrix.android.sdk.internal.util.time.Clock -import timber.log.Timber -import javax.inject.Inject - -internal class VerificationMessageProcessor @Inject constructor( - private val verificationService: DefaultVerificationService, - @UserId private val userId: String, - @DeviceId private val deviceId: String?, - private val clock: Clock, -) { - - private val transactionsHandledByOtherDevice = ArrayList() - - private val allowedTypes = listOf( - EventType.KEY_VERIFICATION_START, - EventType.KEY_VERIFICATION_ACCEPT, - EventType.KEY_VERIFICATION_KEY, - EventType.KEY_VERIFICATION_MAC, - EventType.KEY_VERIFICATION_CANCEL, - EventType.KEY_VERIFICATION_DONE, - EventType.KEY_VERIFICATION_READY, - EventType.MESSAGE, - EventType.ENCRYPTED - ) - - fun shouldProcess(eventType: String): Boolean { - return allowedTypes.contains(eventType) - } - - suspend fun process(roomId: String, event: Event) { - Timber.v("## SAS Verification[${userId.take(5)}] live observer: received msgId: ${event.eventId} msgtype: ${event.getClearType()} from ${event.senderId}") - - // If the request is in the future by more than 5 minutes or more than 10 minutes in the past, - // the message should be ignored by the receiver. - - if (!VerificationService.isValidRequest(event.ageLocalTs, clock.epochMillis())) return Unit.also { - Timber.d("## SAS Verification[${userId.take(5)}] live observer: msgId: ${event.eventId} is outdated age:${event.ageLocalTs} ms") - } - - Timber.v("## SAS Verification[${userId.take(5)}] live observer: received msgId: ${event.eventId} type: ${event.getClearType()}") - - // Relates to is not encrypted - val relatesToEventId = event.getRelationContent()?.eventId - - if (event.senderId == userId) { - // If it's send from me, we need to keep track of Requests or Start - // done from another device of mine -// if (EventType.MESSAGE == event.getClearType()) { -// val msgType = event.getClearContent().toModel()?.msgType -// if (MessageType.MSGTYPE_VERIFICATION_REQUEST == msgType) { -// event.getClearContent().toModel()?.let { -// if (it.fromDevice != deviceId) { -// // The verification is requested from another device -// Timber.v("## SAS Verification[$userItakeng5 live observer: Transaction requested from other device tid:${event.eventId} ") -// event.eventId?.let { txId -> transactionsHandledByOtherDevice.add(txId) } -// } -// } -// } -// } else if (EventType.KEY_VERIFICATION_START == event.getClearType()) { -// event.getClearContent().toModel()?.let { -// if (it.fromDevice != deviceId) { -// // The verification is started from another device -// Timber.v("## SAS Verification[$userItakeng5 live observer: Transaction started by other device tid:$relatesToEventId ") -// relatesToEventId?.let { txId -> transactionsHandledByOtherDevice.add(txId) } -// verificationService.onRoomRequestHandledByOtherDevice(event) -// } -// } -// } else - // we only care about room ready sent by me - if (EventType.KEY_VERIFICATION_READY == event.getClearType()) { - event.getClearContent().toModel()?.let { - if (it.fromDevice != deviceId) { - // The verification is started from another device - Timber.v("## SAS Verification[${userId.take(5)}] live observer: Transaction started by other device tid:$relatesToEventId ") - relatesToEventId?.let { txId -> transactionsHandledByOtherDevice.add(txId) } - verificationService.onRoomReadyFromOneOfMyOtherDevice(event) - } - } - } -// else { -// Timber.v("## SAS Verification[${userId.take(5)}] ignoring message sent by me: ${event.eventId} type: ${event.getClearType()}") -// } -// } else if (EventType.KEY_VERIFICATION_CANCEL == event.getClearType() || EventType.KEY_VERIFICATION_DONE == event.getClearType()) { -// relatesToEventId?.let { -// transactionsHandledByOtherDevice.remove(it) -// verificationService.onRoomRequestHandledByOtherDevice(event) -// } -// } else if (EventType.ENCRYPTED == event.getClearType()) { -// verificationService.onPotentiallyInterestingEventRoomFailToDecrypt(event) -// } - Timber.v("## SAS Verification[${userId.take(5)}] discard from me msgId: ${event.eventId}") - return - } - - if (relatesToEventId != null && transactionsHandledByOtherDevice.contains(relatesToEventId)) { - // Ignore this event, it is directed to another of my devices - Timber.v("## SAS Verification[${userId.take(5)}] live observer: Ignore Transaction handled by other device tid:$relatesToEventId ") - return - } - when (event.getClearType()) { - EventType.KEY_VERIFICATION_START, - EventType.KEY_VERIFICATION_ACCEPT, - EventType.KEY_VERIFICATION_KEY, - EventType.KEY_VERIFICATION_MAC, - EventType.KEY_VERIFICATION_CANCEL, - EventType.KEY_VERIFICATION_READY, - EventType.KEY_VERIFICATION_DONE -> { - verificationService.onRoomEvent(roomId, event) - } - EventType.MESSAGE -> { - if (MessageType.MSGTYPE_VERIFICATION_REQUEST == event.getClearContent().toModel()?.msgType) { - verificationService.onRoomRequestReceived(roomId, event) - } - } - } - } -} diff --git a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/VerificationRequestsStore.kt b/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/VerificationRequestsStore.kt deleted file mode 100644 index 5a4748c5b4..0000000000 --- a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/VerificationRequestsStore.kt +++ /dev/null @@ -1,103 +0,0 @@ -/* - * Copyright 2022 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. - */ - -package org.matrix.android.sdk.internal.crypto.verification - -import org.matrix.android.sdk.api.session.crypto.verification.PendingVerificationRequest -import org.matrix.android.sdk.api.session.crypto.verification.VerificationTransaction -import javax.inject.Inject - -internal class VerificationRequestsStore @Inject constructor() { - - // map [sender : [transaction]] - private val txMap = HashMap>() - - // we need to keep track of finished transaction - // It will be used for gossiping (to send request after request is completed and 'done' by other) - private val pastTransactions = HashMap>() - - /** - * Map [sender: [PendingVerificationRequest]] - * For now we keep all requests (even terminated ones) during the lifetime of the app. - */ - private val pendingRequests = HashMap>() - - fun getExistingRequest(fromUser: String, requestId: String): KotlinVerificationRequest? { - return pendingRequests[fromUser]?.firstOrNull { it.requestId == requestId } - } - - fun getExistingRequestsForUser(fromUser: String): List { - return pendingRequests[fromUser].orEmpty() - } - - fun getExistingRequestInRoom(requestId: String, roomId: String): KotlinVerificationRequest? { - return pendingRequests.flatMap { entry -> - entry.value.filter { it.roomId == roomId && it.requestId == requestId } - }.firstOrNull() - } - - fun getExistingRequestWithRequestId(requestId: String): KotlinVerificationRequest? { - return pendingRequests - .flatMap { it.value } - .firstOrNull { it.requestId == requestId } - } - - fun getExistingTransaction(fromUser: String, transactionId: String): VerificationTransaction? { - return txMap[fromUser]?.get(transactionId) - } - - fun getExistingTransaction(transactionId: String): VerificationTransaction? { - txMap.forEach { - val match = it.value.values - .firstOrNull { it.transactionId == transactionId } - if (match != null) return match - } - return null - } - - fun deleteTransaction(fromUser: String, transactionId: String) { - txMap[fromUser]?.remove(transactionId) - } - - fun deleteRequest(request: PendingVerificationRequest) { - val requestsForUser = pendingRequests.getOrPut(request.otherUserId) { mutableListOf() } - val index = requestsForUser.indexOfFirst { - it.requestId == request.transactionId - } - if (index != -1) { - requestsForUser.removeAt(index) - } - } - -// fun deleteRequest(otherUserId: String, transactionId: String) { -// txMap[otherUserId]?.remove(transactionId) -// } - - fun addRequest(otherUserId: String, request: KotlinVerificationRequest) { - pendingRequests.getOrPut(otherUserId) { mutableListOf() } - .add(request) - } - - fun addTransaction(transaction: VerificationTransaction) { - val txInnerMap = txMap.getOrPut(transaction.otherUserId) { mutableMapOf() } - txInnerMap[transaction.transactionId] = transaction - } - - fun rememberPastSuccessfulTransaction(transaction: VerificationTransaction) { - val transactionId = transaction.transactionId - pastTransactions.getOrPut(transactionId) { mutableMapOf() }[transactionId] = transaction - } -} diff --git a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/VerificationTransportLayer.kt b/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/VerificationTransportLayer.kt deleted file mode 100644 index b1648a1e71..0000000000 --- a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/VerificationTransportLayer.kt +++ /dev/null @@ -1,109 +0,0 @@ -/* - * Copyright 2022 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. - */ - -package org.matrix.android.sdk.internal.crypto.verification - -import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap -import org.matrix.android.sdk.api.session.crypto.model.SendToDeviceObject -import org.matrix.android.sdk.api.session.events.model.Content -import org.matrix.android.sdk.api.session.events.model.Event -import org.matrix.android.sdk.api.session.events.model.LocalEcho -import org.matrix.android.sdk.api.session.events.model.UnsignedData -import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask -import org.matrix.android.sdk.internal.crypto.tasks.SendVerificationMessageTask -import org.matrix.android.sdk.internal.di.UserId -import org.matrix.android.sdk.internal.session.room.send.LocalEchoEventFactory -import org.matrix.android.sdk.internal.util.time.Clock -import javax.inject.Inject - -internal class VerificationTransportLayer @Inject constructor( - @UserId private val myUserId: String, - private val sendVerificationMessageTask: SendVerificationMessageTask, - private val localEchoEventFactory: LocalEchoEventFactory, - private val sendToDeviceTask: SendToDeviceTask, - private val clock: Clock, -) { - - suspend fun sendToOther( - request: KotlinVerificationRequest, - type: String, - verificationInfo: VerificationInfo<*>, - ) { - val roomId = request.roomId - if (roomId != null) { - val event = createEventAndLocalEcho( - type = type, - roomId = roomId, - content = verificationInfo.toEventContent()!! - ) - sendEventInRoom(event) - } else { - sendToDeviceEvent( - type, - verificationInfo.toSendToDeviceObject()!!, - request.otherUserId, - request.otherDeviceId()?.let { listOf(it) }.orEmpty() - ) - } - } - - private fun createEventAndLocalEcho(localId: String = LocalEcho.createLocalEchoId(), - type: String, - roomId: String, - content: Content): Event { - return Event( - roomId = roomId, - originServerTs = clock.epochMillis(), - senderId = myUserId, - eventId = localId, - type = type, - content = content, - unsignedData = UnsignedData(age = null, transactionId = localId) - ).also { - localEchoEventFactory.createLocalEcho(it) - } - } - - suspend fun sendInRoom(type: String, - roomId: String, - content: Content): String { - val event = createEventAndLocalEcho( - type = type, - roomId = roomId, - content = content - ) - return sendEventInRoom(event) - } - - suspend fun sendEventInRoom(event: Event): String { - return sendVerificationMessageTask.execute(SendVerificationMessageTask.Params(event, 5)).eventId - } - - suspend fun sendToDeviceEvent(messageType: String, toSendToDeviceObject: SendToDeviceObject, otherUserId: String, targetDevices: List) { - // currently to device verification messages are sent unencrypted - // as per spec not recommended - // > verification messages may be sent unencrypted, though this is not encouraged. - - val contentMap = MXUsersDevicesMap() - - targetDevices.forEach { - contentMap.setObject(otherUserId, it, toSendToDeviceObject) - } - - sendToDeviceTask - .execute(SendToDeviceTask.Params(messageType, contentMap)) - } -} diff --git a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/VerificationTrustBackend.kt b/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/VerificationTrustBackend.kt deleted file mode 100644 index a478e38215..0000000000 --- a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/VerificationTrustBackend.kt +++ /dev/null @@ -1,95 +0,0 @@ -/* - * Copyright 2022 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. - */ - -package org.matrix.android.sdk.internal.crypto.verification - -import org.matrix.android.sdk.api.session.crypto.crosssigning.CrossSigningService -import org.matrix.android.sdk.api.session.crypto.crosssigning.DeviceTrustLevel -import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupService -import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo -import org.matrix.android.sdk.internal.crypto.actions.SetDeviceVerificationAction -import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore -import org.matrix.android.sdk.internal.di.DeviceId -import org.matrix.android.sdk.internal.di.UserId -import javax.inject.Inject - -internal class VerificationTrustBackend @Inject constructor( - private val crossSigningService: dagger.Lazy, - private val setDeviceVerificationAction: SetDeviceVerificationAction, - private val keysBackupService: dagger.Lazy, - private val cryptoStore: IMXCryptoStore, - @UserId private val myUserId: String, - @DeviceId private val myDeviceId: String, -) { - - suspend fun getUserMasterKeyBase64(userId: String): String? { - return crossSigningService.get()?.getUserCrossSigningKeys(userId)?.masterKey()?.unpaddedBase64PublicKey - } - - suspend fun getMyTrustedMasterKeyBase64(): String? { - return cryptoStore.getMyCrossSigningInfo() - ?.takeIf { it.isTrusted() } - ?.masterKey() - ?.unpaddedBase64PublicKey - } - - fun canCrossSign(): Boolean { - return crossSigningService.get().canCrossSign() - } - - suspend fun trustUser(userId: String) { - crossSigningService.get().trustUser(userId) - } - - suspend fun trustOwnDevice(deviceId: String) { - crossSigningService.get().trustDevice(deviceId) - } - - suspend fun locallyTrustDevice(otherUserId: String, deviceId: String) { - val actualTrustLevel = getUserDevice(otherUserId, deviceId)?.trustLevel - setDeviceVerificationAction.handle( - trustLevel = DeviceTrustLevel( - actualTrustLevel?.crossSigningVerified == true, - true - ), - userId = otherUserId, - deviceId = deviceId - ) - } - - suspend fun markMyMasterKeyAsTrusted() { - crossSigningService.get().markMyMasterKeyAsTrusted() - keysBackupService.get().checkAndStartKeysBackup() - } - - fun getUserDevice(userId: String, deviceId: String): CryptoDeviceInfo? { - return cryptoStore.getUserDevice(userId, deviceId) - } - - fun getMyDevice(): CryptoDeviceInfo { - return getUserDevice(myUserId, myDeviceId)!! - } - - fun getUserDeviceList(userId: String): List { - return cryptoStore.getUserDeviceList(userId).orEmpty() - } -// -// suspend fun areMyCrossSigningKeysTrusted() : Boolean { -// return crossSigningService.get().isUserTrusted(myUserId) -// } - - fun getMyDeviceId() = myDeviceId -} diff --git a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/Extensions.kt b/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/Extensions.kt deleted file mode 100644 index a0202485d6..0000000000 --- a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/Extensions.kt +++ /dev/null @@ -1,127 +0,0 @@ -/* - * Copyright 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. - */ - -package org.matrix.android.sdk.internal.crypto.verification.qrcode - -import org.matrix.android.sdk.api.util.fromBase64 -import org.matrix.android.sdk.api.util.toBase64NoPadding -import org.matrix.android.sdk.internal.extensions.toUnsignedInt - -// MATRIX -private val prefix = "MATRIX".toByteArray(Charsets.ISO_8859_1) - -internal fun QrCodeData.toEncodedString(): String { - var result = ByteArray(0) - - // MATRIX - for (i in prefix.indices) { - result += prefix[i] - } - - // Version - result += 2 - - // Mode - result += when (this) { - is QrCodeData.VerifyingAnotherUser -> 0 - is QrCodeData.SelfVerifyingMasterKeyTrusted -> 1 - is QrCodeData.SelfVerifyingMasterKeyNotTrusted -> 2 - }.toByte() - - // TransactionId length - val length = transactionId.length - result += ((length and 0xFF00) shr 8).toByte() - result += length.toByte() - - // TransactionId - transactionId.forEach { - result += it.code.toByte() - } - - // Keys - firstKey.fromBase64().forEach { - result += it - } - secondKey.fromBase64().forEach { - result += it - } - - // Secret - sharedSecret.fromBase64().forEach { - result += it - } - - return result.toString(Charsets.ISO_8859_1) -} - -internal fun String.toQrCodeData(): QrCodeData? { - val byteArray = toByteArray(Charsets.ISO_8859_1) - - // Size should be min 6 + 1 + 1 + 2 + ? + 32 + 32 + ? = 74 + transactionLength + secretLength - - // Check header - // MATRIX - if (byteArray.size < 10) return null - - for (i in prefix.indices) { - if (byteArray[i] != prefix[i]) { - return null - } - } - - var cursor = prefix.size // 6 - - // Version - if (byteArray[cursor] != 2.toByte()) { - return null - } - cursor++ - - // Get mode - val mode = byteArray[cursor].toInt() - cursor++ - - // Get transaction length, Big Endian format - val msb = byteArray[cursor].toUnsignedInt() - val lsb = byteArray[cursor + 1].toUnsignedInt() - - val transactionLength = msb.shl(8) + lsb - - cursor++ - cursor++ - - val secretLength = byteArray.size - 74 - transactionLength - - // ensure the secret length is 8 bytes min - if (secretLength < 8) { - return null - } - - val transactionId = byteArray.copyOfRange(cursor, cursor + transactionLength).toString(Charsets.ISO_8859_1) - cursor += transactionLength - val key1 = byteArray.copyOfRange(cursor, cursor + 32).toBase64NoPadding() - cursor += 32 - val key2 = byteArray.copyOfRange(cursor, cursor + 32).toBase64NoPadding() - cursor += 32 - val secret = byteArray.copyOfRange(cursor, byteArray.size).toBase64NoPadding() - - return when (mode) { - 0 -> QrCodeData.VerifyingAnotherUser(transactionId, key1, key2, secret) - 1 -> QrCodeData.SelfVerifyingMasterKeyTrusted(transactionId, key1, key2, secret) - 2 -> QrCodeData.SelfVerifyingMasterKeyNotTrusted(transactionId, key1, key2, secret) - else -> null - } -} diff --git a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/QrCodeData.kt b/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/QrCodeData.kt deleted file mode 100644 index f308807e04..0000000000 --- a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/QrCodeData.kt +++ /dev/null @@ -1,105 +0,0 @@ -/* - * Copyright 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. - */ - -package org.matrix.android.sdk.internal.crypto.verification.qrcode - -/** - * Ref: https://github.com/uhoreg/matrix-doc/blob/qr_key_verification/proposals/1543-qr_code_key_verification.md#qr-code-format - */ -internal sealed class QrCodeData( - /** - * the event ID or transaction_id of the associated verification. - */ - open val transactionId: String, - /** - * First key (32 bytes, in base64 no padding). - */ - val firstKey: String, - /** - * Second key (32 bytes, in base64 no padding). - */ - val secondKey: String, - /** - * a random shared secret (in base64 no padding). - */ - open val sharedSecret: String -) { - /** - * Verifying another user with cross-signing - * QR code verification mode: 0x00. - */ - data class VerifyingAnotherUser( - override val transactionId: String, - /** - * the user's own master cross-signing public key. - */ - val userMasterCrossSigningPublicKey: String, - /** - * what the device thinks the other user's master cross-signing key is. - */ - val otherUserMasterCrossSigningPublicKey: String, - override val sharedSecret: String - ) : QrCodeData( - transactionId, - userMasterCrossSigningPublicKey, - otherUserMasterCrossSigningPublicKey, - sharedSecret - ) - - /** - * self-verifying in which the current device does trust the master key - * QR code verification mode: 0x01. - */ - data class SelfVerifyingMasterKeyTrusted( - override val transactionId: String, - /** - * the user's own master cross-signing public key. - */ - val userMasterCrossSigningPublicKey: String, - /** - * what the device thinks the other device's device key is. - */ - val otherDeviceKey: String, - override val sharedSecret: String - ) : QrCodeData( - transactionId, - userMasterCrossSigningPublicKey, - otherDeviceKey, - sharedSecret - ) - - /** - * self-verifying in which the current device does not yet trust the master key - * QR code verification mode: 0x02. - */ - data class SelfVerifyingMasterKeyNotTrusted( - override val transactionId: String, - /** - * the current device's device key. - */ - val deviceKey: String, - /** - * what the device thinks the user's master cross-signing key is. - */ - val userMasterCrossSigningPublicKey: String, - override val sharedSecret: String - ) : QrCodeData( - transactionId, - deviceKey, - userMasterCrossSigningPublicKey, - sharedSecret - ) -} diff --git a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/SharedSecret.kt b/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/SharedSecret.kt deleted file mode 100644 index 52b09be49c..0000000000 --- a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/SharedSecret.kt +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright 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. - */ - -package org.matrix.android.sdk.internal.crypto.verification.qrcode - -import org.matrix.android.sdk.api.util.toBase64NoPadding -import java.security.SecureRandom - -internal fun generateSharedSecretV2(): String { - val secureRandom = SecureRandom() - - // 8 bytes long - val secretBytes = ByteArray(8) - secureRandom.nextBytes(secretBytes) - return secretBytes.toBase64NoPadding() -} diff --git a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/session/sync/handler/CryptoSyncHandler.kt b/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/session/sync/handler/CryptoSyncHandler.kt deleted file mode 100644 index f3e5180b93..0000000000 --- a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/session/sync/handler/CryptoSyncHandler.kt +++ /dev/null @@ -1,130 +0,0 @@ -/* - * Copyright 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. - */ - -package org.matrix.android.sdk.internal.session.sync.handler - -import dagger.Lazy -import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_OLM -import org.matrix.android.sdk.api.logger.LoggerTag -import org.matrix.android.sdk.api.session.crypto.MXCryptoError -import org.matrix.android.sdk.api.session.crypto.model.MXEventDecryptionResult -import org.matrix.android.sdk.api.session.crypto.model.OlmDecryptionResult -import org.matrix.android.sdk.api.session.events.model.Event -import org.matrix.android.sdk.api.session.events.model.EventType -import org.matrix.android.sdk.api.session.events.model.content.OlmEventContent -import org.matrix.android.sdk.api.session.events.model.toModel -import org.matrix.android.sdk.api.session.room.model.message.MessageContent -import org.matrix.android.sdk.internal.crypto.DefaultCryptoService -import org.matrix.android.sdk.internal.crypto.tasks.toDeviceTracingId -import org.matrix.android.sdk.internal.crypto.verification.DefaultVerificationService -import org.matrix.android.sdk.internal.session.sync.ProgressReporter -import timber.log.Timber -import javax.inject.Inject - -private val loggerTag = LoggerTag("CryptoSyncHandler", LoggerTag.CRYPTO) - -internal class CryptoSyncHandler @Inject constructor( - private val cryptoService: Lazy, - private val verificationService: DefaultVerificationService -) { - - suspend fun handleToDevice(eventList: List, progressReporter: ProgressReporter? = null) { - val total = eventList.size - eventList.filter { isSupportedToDevice(it) } - .forEachIndexed { index, event -> - progressReporter?.reportProgress(index * 100F / total) - // Decrypt event if necessary - Timber.tag(loggerTag.value).d("To device event msgid:${event.toDeviceTracingId()}") - decryptToDeviceEvent(event, null) - - if (event.getClearType() == EventType.MESSAGE && - event.getClearContent()?.toModel()?.msgType == "m.bad.encrypted") { - Timber.tag(loggerTag.value).e("handleToDeviceEvent() : Warning: Unable to decrypt to-device event : ${event.content}") - } else { - Timber.tag(loggerTag.value).d("received to-device ${event.getClearType()} from:${event.senderId} msgid:${event.toDeviceTracingId()}") - verificationService.onToDeviceEvent(event) - cryptoService.get().onToDeviceEvent(event) - } - } - } - - private val unsupportedPlainToDeviceEventTypes = listOf( - EventType.ROOM_KEY, - EventType.FORWARDED_ROOM_KEY, - EventType.SEND_SECRET - ) - - private fun isSupportedToDevice(event: Event): Boolean { - val algorithm = event.content?.get("algorithm") as? String - val type = event.type.orEmpty() - return if (event.isEncrypted()) { - algorithm == MXCRYPTO_ALGORITHM_OLM - } else { - // some clear events are not allowed - type !in unsupportedPlainToDeviceEventTypes - }.also { - if (!it) { - Timber.tag(loggerTag.value) - .w("Ignoring unsupported to device event ${event.type} alg:${algorithm}") - } - } - } - - /** - * Decrypt an encrypted event. - * - * @param event the event to decrypt - * @param timelineId the timeline identifier - * @return true if the event has been decrypted - */ - private suspend fun decryptToDeviceEvent(event: Event, timelineId: String?): Boolean { - Timber.v("## CRYPTO | decryptToDeviceEvent") - if (event.getClearType() == EventType.ENCRYPTED) { - var result: MXEventDecryptionResult? = null - try { - result = cryptoService.get().decryptEvent(event, timelineId ?: "") - } catch (exception: MXCryptoError) { - event.mCryptoError = (exception as? MXCryptoError.Base)?.errorType // setCryptoError(exception.cryptoError) - val senderKey = event.content.toModel()?.senderKey ?: "" - // try to find device id to ease log reading - val deviceId = cryptoService.get().getCryptoDeviceInfo(event.senderId!!).firstOrNull { - it.identityKey() == senderKey - }?.deviceId ?: senderKey - Timber.e("## CRYPTO | Failed to decrypt to device event from ${event.senderId}|$deviceId reason:<${event.mCryptoError ?: exception}>") - } catch (failure: Throwable) { - Timber.e(failure, "## CRYPTO | Failed to decrypt to device event from ${event.senderId}") - } - - if (null != result) { - event.mxDecryptionResult = OlmDecryptionResult( - payload = result.clearEvent, - senderKey = result.senderCurve25519Key, - keysClaimed = result.claimedEd25519Key?.let { mapOf("ed25519" to it) }, - forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain, - verificationState = result.messageVerificationState, - ) - return true - } else { - // Could happen for to device events - // None of the known session could decrypt the message - // In this case unwedging process might have been started (rate limited) - Timber.e("## CRYPTO | ERROR NULL DECRYPTION RESULT from ${event.senderId}") - } - } - - return false - } -} diff --git a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/session/sync/handler/ShieldSummaryUpdater.kt b/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/session/sync/handler/ShieldSummaryUpdater.kt deleted file mode 100644 index bcc078b550..0000000000 --- a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/session/sync/handler/ShieldSummaryUpdater.kt +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright 2023 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. - */ - -package org.matrix.android.sdk.internal.session.sync.handler - -import androidx.work.BackoffPolicy -import androidx.work.ExistingWorkPolicy -import org.matrix.android.sdk.internal.crypto.crosssigning.UpdateTrustWorker -import org.matrix.android.sdk.internal.crypto.crosssigning.UpdateTrustWorkerDataRepository -import org.matrix.android.sdk.internal.di.SessionId -import org.matrix.android.sdk.internal.di.WorkManagerProvider -import org.matrix.android.sdk.internal.session.SessionScope -import org.matrix.android.sdk.internal.util.logLimit -import org.matrix.android.sdk.internal.worker.WorkerParamsFactory -import timber.log.Timber -import java.util.concurrent.TimeUnit -import javax.inject.Inject - -@SessionScope -internal class ShieldSummaryUpdater @Inject constructor( - @SessionId private val sessionId: String, - private val workManagerProvider: WorkManagerProvider, - private val updateTrustWorkerDataRepository: UpdateTrustWorkerDataRepository, -) { - - fun refreshShieldsForRoomIds(roomIds: Set) { - Timber.d("## CrossSigning - checkAffectedRoomShields for roomIds: ${roomIds.logLimit()}") - val workerParams = UpdateTrustWorker.Params( - sessionId = sessionId, - filename = updateTrustWorkerDataRepository.createParam(emptyList(), roomIds = roomIds.toList()) - ) - val workerData = WorkerParamsFactory.toData(workerParams) - - val workRequest = workManagerProvider.matrixOneTimeWorkRequestBuilder() - .setInputData(workerData) - .setBackoffCriteria(BackoffPolicy.LINEAR, WorkManagerProvider.BACKOFF_DELAY_MILLIS, TimeUnit.MILLISECONDS) - .build() - - workManagerProvider.workManager - .beginUniqueWork("TRUST_UPDATE_QUEUE", ExistingWorkPolicy.APPEND_OR_REPLACE, workRequest) - .enqueue() - } -} diff --git a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/session/SessionComponent.kt b/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/session/SessionComponent.kt deleted file mode 100644 index 3cfcdac11c..0000000000 --- a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/session/SessionComponent.kt +++ /dev/null @@ -1,145 +0,0 @@ -/* - * Copyright 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. - */ - -package org.matrix.android.sdk.internal.session - -import dagger.BindsInstance -import dagger.Component -import org.matrix.android.sdk.api.MatrixCoroutineDispatchers -import org.matrix.android.sdk.api.auth.data.SessionParams -import org.matrix.android.sdk.api.securestorage.SecureStorageModule -import org.matrix.android.sdk.api.session.Session -import org.matrix.android.sdk.internal.crypto.CryptoModule -import org.matrix.android.sdk.internal.crypto.crosssigning.UpdateTrustWorker -import org.matrix.android.sdk.internal.di.MatrixComponent -import org.matrix.android.sdk.internal.federation.FederationModule -import org.matrix.android.sdk.internal.network.NetworkConnectivityChecker -import org.matrix.android.sdk.internal.network.RequestModule -import org.matrix.android.sdk.internal.session.account.AccountModule -import org.matrix.android.sdk.internal.session.cache.CacheModule -import org.matrix.android.sdk.internal.session.call.CallModule -import org.matrix.android.sdk.internal.session.content.ContentModule -import org.matrix.android.sdk.internal.session.content.UploadContentWorker -import org.matrix.android.sdk.internal.session.contentscanner.ContentScannerModule -import org.matrix.android.sdk.internal.session.filter.FilterModule -import org.matrix.android.sdk.internal.session.homeserver.HomeServerCapabilitiesModule -import org.matrix.android.sdk.internal.session.identity.IdentityModule -import org.matrix.android.sdk.internal.session.integrationmanager.IntegrationManagerModule -import org.matrix.android.sdk.internal.session.media.MediaModule -import org.matrix.android.sdk.internal.session.openid.OpenIdModule -import org.matrix.android.sdk.internal.session.presence.di.PresenceModule -import org.matrix.android.sdk.internal.session.profile.ProfileModule -import org.matrix.android.sdk.internal.session.pushers.AddPusherWorker -import org.matrix.android.sdk.internal.session.pushers.PushersModule -import org.matrix.android.sdk.internal.session.room.RoomModule -import org.matrix.android.sdk.internal.session.room.aggregation.livelocation.DeactivateLiveLocationShareWorker -import org.matrix.android.sdk.internal.session.room.send.MultipleEventSendingDispatcherWorker -import org.matrix.android.sdk.internal.session.room.send.RedactEventWorker -import org.matrix.android.sdk.internal.session.room.send.SendEventWorker -import org.matrix.android.sdk.internal.session.search.SearchModule -import org.matrix.android.sdk.internal.session.signout.SignOutModule -import org.matrix.android.sdk.internal.session.space.SpaceModule -import org.matrix.android.sdk.internal.session.sync.SyncModule -import org.matrix.android.sdk.internal.session.sync.SyncTask -import org.matrix.android.sdk.internal.session.sync.SyncTokenStore -import org.matrix.android.sdk.internal.session.sync.handler.UpdateUserWorker -import org.matrix.android.sdk.internal.session.sync.job.SyncWorker -import org.matrix.android.sdk.internal.session.terms.TermsModule -import org.matrix.android.sdk.internal.session.thirdparty.ThirdPartyModule -import org.matrix.android.sdk.internal.session.user.UserModule -import org.matrix.android.sdk.internal.session.user.accountdata.AccountDataModule -import org.matrix.android.sdk.internal.session.widgets.WidgetModule -import org.matrix.android.sdk.internal.task.TaskExecutor -import org.matrix.android.sdk.internal.util.system.SystemModule - -@Component( - dependencies = [MatrixComponent::class], - modules = [ - SessionModule::class, - RoomModule::class, - SyncModule::class, - HomeServerCapabilitiesModule::class, - SignOutModule::class, - UserModule::class, - FilterModule::class, - ContentModule::class, - CacheModule::class, - MediaModule::class, - CryptoModule::class, - SystemModule::class, - PushersModule::class, - OpenIdModule::class, - WidgetModule::class, - IntegrationManagerModule::class, - IdentityModule::class, - TermsModule::class, - AccountDataModule::class, - ProfileModule::class, - AccountModule::class, - FederationModule::class, - CallModule::class, - ContentScannerModule::class, - SearchModule::class, - ThirdPartyModule::class, - SpaceModule::class, - PresenceModule::class, - RequestModule::class, - SecureStorageModule::class, - ] -) -@SessionScope -internal interface SessionComponent { - - fun coroutineDispatchers(): MatrixCoroutineDispatchers - - fun session(): Session - - fun syncTask(): SyncTask - - fun syncTokenStore(): SyncTokenStore - - fun networkConnectivityChecker(): NetworkConnectivityChecker - - // fun olmMachine(): OlmMachine - - fun taskExecutor(): TaskExecutor - - fun inject(worker: SendEventWorker) - - fun inject(worker: MultipleEventSendingDispatcherWorker) - - fun inject(worker: RedactEventWorker) - - fun inject(worker: UploadContentWorker) - - fun inject(worker: SyncWorker) - - fun inject(worker: AddPusherWorker) - - fun inject(worker: UpdateTrustWorker) - - fun inject(worker: UpdateUserWorker) - - fun inject(worker: DeactivateLiveLocationShareWorker) - - @Component.Factory - interface Factory { - fun create( - matrixComponent: MatrixComponent, - @BindsInstance sessionParams: SessionParams - ): SessionComponent - } -} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/MatrixConfiguration.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/MatrixConfiguration.kt index d4573b02b2..52d4d70b73 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/MatrixConfiguration.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/MatrixConfiguration.kt @@ -29,7 +29,6 @@ import java.net.Proxy data class MatrixConfiguration( val applicationFlavor: String = "Default-application-flavor", val cryptoConfig: MXCryptoConfig = MXCryptoConfig(), - val cryptoFlavor: String = "Default-crypto-flavor", val integrationUIUrl: String = "https://scalar.vector.im/", val integrationRestUrl: String = "https://scalar.vector.im/api", val integrationWidgetUrls: List = listOf( diff --git a/matrix-sdk-android/src/rustCrypto/java/org/matrix/android/sdk/internal/crypto/keysbackup/BackupRecoveryKey.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/keysbackup/BackupRecoveryKey.kt similarity index 100% rename from matrix-sdk-android/src/rustCrypto/java/org/matrix/android/sdk/internal/crypto/keysbackup/BackupRecoveryKey.kt rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/keysbackup/BackupRecoveryKey.kt diff --git a/matrix-sdk-android/src/rustCrypto/java/org/matrix/android/sdk/api/session/crypto/keysbackup/BackupUtils.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/keysbackup/BackupUtils.kt similarity index 82% rename from matrix-sdk-android/src/rustCrypto/java/org/matrix/android/sdk/api/session/crypto/keysbackup/BackupUtils.kt rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/keysbackup/BackupUtils.kt index 8b35586c4f..f3f4e23b21 100644 --- a/matrix-sdk-android/src/rustCrypto/java/org/matrix/android/sdk/api/session/crypto/keysbackup/BackupUtils.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/keysbackup/BackupUtils.kt @@ -17,6 +17,6 @@ package org.matrix.android.sdk.api.session.crypto.keysbackup object BackupUtils { - fun recoveryKeyFromBase58(key: String): IBackupRecoveryKey? = BackupRecoveryKey.fromBase58(key) - fun recoveryKeyFromPassphrase(passphrase: String): IBackupRecoveryKey? = BackupRecoveryKey.newFromPassphrase(passphrase) + fun recoveryKeyFromBase58(key: String): IBackupRecoveryKey = BackupRecoveryKey.fromBase58(key) + fun recoveryKeyFromPassphrase(passphrase: String): IBackupRecoveryKey = BackupRecoveryKey.newFromPassphrase(passphrase) } diff --git a/matrix-sdk-android/src/rustCrypto/java/org/matrix/android/sdk/api/session/room/model/message/MessageVerificationCancelContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageVerificationCancelContent.kt similarity index 100% rename from matrix-sdk-android/src/rustCrypto/java/org/matrix/android/sdk/api/session/room/model/message/MessageVerificationCancelContent.kt rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageVerificationCancelContent.kt diff --git a/matrix-sdk-android/src/rustCrypto/java/org/matrix/android/sdk/api/session/room/model/message/MessageVerificationDoneContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageVerificationDoneContent.kt similarity index 100% rename from matrix-sdk-android/src/rustCrypto/java/org/matrix/android/sdk/api/session/room/model/message/MessageVerificationDoneContent.kt rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageVerificationDoneContent.kt diff --git a/matrix-sdk-android/src/rustCrypto/java/org/matrix/android/sdk/api/session/room/model/message/MessageVerificationKeyContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageVerificationKeyContent.kt similarity index 100% rename from matrix-sdk-android/src/rustCrypto/java/org/matrix/android/sdk/api/session/room/model/message/MessageVerificationKeyContent.kt rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageVerificationKeyContent.kt diff --git a/matrix-sdk-android/src/rustCrypto/java/org/matrix/android/sdk/api/session/room/model/message/MessageVerificationMacContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageVerificationMacContent.kt similarity index 100% rename from matrix-sdk-android/src/rustCrypto/java/org/matrix/android/sdk/api/session/room/model/message/MessageVerificationMacContent.kt rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageVerificationMacContent.kt diff --git a/matrix-sdk-android/src/rustCrypto/java/org/matrix/android/sdk/api/session/room/model/message/MessageVerificationReadyContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageVerificationReadyContent.kt similarity index 100% rename from matrix-sdk-android/src/rustCrypto/java/org/matrix/android/sdk/api/session/room/model/message/MessageVerificationReadyContent.kt rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageVerificationReadyContent.kt diff --git a/matrix-sdk-android/src/rustCrypto/java/org/matrix/android/sdk/api/session/room/model/message/MessageVerificationRequestContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageVerificationRequestContent.kt similarity index 100% rename from matrix-sdk-android/src/rustCrypto/java/org/matrix/android/sdk/api/session/room/model/message/MessageVerificationRequestContent.kt rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageVerificationRequestContent.kt diff --git a/matrix-sdk-android/src/rustCrypto/java/org/matrix/android/sdk/api/session/room/model/message/MessageVerificationStartContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageVerificationStartContent.kt similarity index 100% rename from matrix-sdk-android/src/rustCrypto/java/org/matrix/android/sdk/api/session/room/model/message/MessageVerificationStartContent.kt rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageVerificationStartContent.kt diff --git a/matrix-sdk-android/src/rustCrypto/java/org/matrix/android/sdk/internal/coroutines/builder/FlowBuilders.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/coroutines/builder/FlowBuilders.kt similarity index 100% rename from matrix-sdk-android/src/rustCrypto/java/org/matrix/android/sdk/internal/coroutines/builder/FlowBuilders.kt rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/coroutines/builder/FlowBuilders.kt diff --git a/matrix-sdk-android/src/rustCrypto/java/org/matrix/android/sdk/internal/crypto/ComputeShieldForGroupUseCase.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/ComputeShieldForGroupUseCase.kt similarity index 100% rename from matrix-sdk-android/src/rustCrypto/java/org/matrix/android/sdk/internal/crypto/ComputeShieldForGroupUseCase.kt rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/ComputeShieldForGroupUseCase.kt diff --git a/matrix-sdk-android/src/rustCrypto/java/org/matrix/android/sdk/internal/crypto/CryptoModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/CryptoModule.kt similarity index 100% rename from matrix-sdk-android/src/rustCrypto/java/org/matrix/android/sdk/internal/crypto/CryptoModule.kt rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/CryptoModule.kt diff --git a/matrix-sdk-android/src/rustCrypto/java/org/matrix/android/sdk/internal/crypto/DecryptRoomEventUseCase.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DecryptRoomEventUseCase.kt similarity index 100% rename from matrix-sdk-android/src/rustCrypto/java/org/matrix/android/sdk/internal/crypto/DecryptRoomEventUseCase.kt rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DecryptRoomEventUseCase.kt diff --git a/matrix-sdk-android/src/rustCrypto/java/org/matrix/android/sdk/internal/crypto/Device.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/Device.kt similarity index 100% rename from matrix-sdk-android/src/rustCrypto/java/org/matrix/android/sdk/internal/crypto/Device.kt rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/Device.kt diff --git a/matrix-sdk-android/src/rustCrypto/java/org/matrix/android/sdk/internal/crypto/EncryptEventContentUseCase.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/EncryptEventContentUseCase.kt similarity index 100% rename from matrix-sdk-android/src/rustCrypto/java/org/matrix/android/sdk/internal/crypto/EncryptEventContentUseCase.kt rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/EncryptEventContentUseCase.kt diff --git a/matrix-sdk-android/src/rustCrypto/java/org/matrix/android/sdk/internal/crypto/EnsureUsersKeysUseCase.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/EnsureUsersKeysUseCase.kt similarity index 100% rename from matrix-sdk-android/src/rustCrypto/java/org/matrix/android/sdk/internal/crypto/EnsureUsersKeysUseCase.kt rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/EnsureUsersKeysUseCase.kt diff --git a/matrix-sdk-android/src/rustCrypto/java/org/matrix/android/sdk/internal/crypto/EventDecryptor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/EventDecryptor.kt similarity index 100% rename from matrix-sdk-android/src/rustCrypto/java/org/matrix/android/sdk/internal/crypto/EventDecryptor.kt rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/EventDecryptor.kt diff --git a/matrix-sdk-android/src/rustCrypto/java/org/matrix/android/sdk/internal/crypto/FlowCollectors.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/FlowCollectors.kt similarity index 100% rename from matrix-sdk-android/src/rustCrypto/java/org/matrix/android/sdk/internal/crypto/FlowCollectors.kt rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/FlowCollectors.kt diff --git a/matrix-sdk-android/src/rustCrypto/java/org/matrix/android/sdk/internal/crypto/GetUserIdentityUseCase.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/GetUserIdentityUseCase.kt similarity index 100% rename from matrix-sdk-android/src/rustCrypto/java/org/matrix/android/sdk/internal/crypto/GetUserIdentityUseCase.kt rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/GetUserIdentityUseCase.kt diff --git a/matrix-sdk-android/src/rustCrypto/java/org/matrix/android/sdk/internal/crypto/OlmMachine.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OlmMachine.kt similarity index 100% rename from matrix-sdk-android/src/rustCrypto/java/org/matrix/android/sdk/internal/crypto/OlmMachine.kt rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OlmMachine.kt diff --git a/matrix-sdk-android/src/rustCrypto/java/org/matrix/android/sdk/internal/crypto/PrepareToEncryptUseCase.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/PrepareToEncryptUseCase.kt similarity index 100% rename from matrix-sdk-android/src/rustCrypto/java/org/matrix/android/sdk/internal/crypto/PrepareToEncryptUseCase.kt rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/PrepareToEncryptUseCase.kt diff --git a/matrix-sdk-android/src/rustCrypto/java/org/matrix/android/sdk/internal/crypto/RustCrossSigningService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/RustCrossSigningService.kt similarity index 100% rename from matrix-sdk-android/src/rustCrypto/java/org/matrix/android/sdk/internal/crypto/RustCrossSigningService.kt rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/RustCrossSigningService.kt diff --git a/matrix-sdk-android/src/rustCrypto/java/org/matrix/android/sdk/internal/crypto/RustCryptoService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/RustCryptoService.kt similarity index 100% rename from matrix-sdk-android/src/rustCrypto/java/org/matrix/android/sdk/internal/crypto/RustCryptoService.kt rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/RustCryptoService.kt diff --git a/matrix-sdk-android/src/rustCrypto/java/org/matrix/android/sdk/internal/crypto/SecretShareManager.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/SecretShareManager.kt similarity index 100% rename from matrix-sdk-android/src/rustCrypto/java/org/matrix/android/sdk/internal/crypto/SecretShareManager.kt rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/SecretShareManager.kt diff --git a/matrix-sdk-android/src/rustCrypto/java/org/matrix/android/sdk/internal/crypto/UserIdentities.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/UserIdentities.kt similarity index 100% rename from matrix-sdk-android/src/rustCrypto/java/org/matrix/android/sdk/internal/crypto/UserIdentities.kt rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/UserIdentities.kt diff --git a/matrix-sdk-android/src/rustCrypto/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/UnRequestedForwardManager.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/UnRequestedForwardManager.kt similarity index 100% rename from matrix-sdk-android/src/rustCrypto/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/UnRequestedForwardManager.kt rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/UnRequestedForwardManager.kt diff --git a/matrix-sdk-android/src/rustCrypto/java/org/matrix/android/sdk/internal/crypto/crosssigning/UpdateTrustWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/UpdateTrustWorker.kt similarity index 100% rename from matrix-sdk-android/src/rustCrypto/java/org/matrix/android/sdk/internal/crypto/crosssigning/UpdateTrustWorker.kt rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/UpdateTrustWorker.kt diff --git a/matrix-sdk-android/src/rustCrypto/java/org/matrix/android/sdk/internal/crypto/keysbackup/RustKeyBackupService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/RustKeyBackupService.kt similarity index 100% rename from matrix-sdk-android/src/rustCrypto/java/org/matrix/android/sdk/internal/crypto/keysbackup/RustKeyBackupService.kt rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/RustKeyBackupService.kt diff --git a/matrix-sdk-android/src/rustCrypto/java/org/matrix/android/sdk/internal/crypto/network/OutgoingRequestsProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/network/OutgoingRequestsProcessor.kt similarity index 100% rename from matrix-sdk-android/src/rustCrypto/java/org/matrix/android/sdk/internal/crypto/network/OutgoingRequestsProcessor.kt rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/network/OutgoingRequestsProcessor.kt diff --git a/matrix-sdk-android/src/rustCrypto/java/org/matrix/android/sdk/internal/crypto/network/RequestSender.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/network/RequestSender.kt similarity index 100% rename from matrix-sdk-android/src/rustCrypto/java/org/matrix/android/sdk/internal/crypto/network/RequestSender.kt rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/network/RequestSender.kt diff --git a/matrix-sdk-android/src/rustCrypto/java/org/matrix/android/sdk/internal/crypto/store/RustCryptoStore.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/RustCryptoStore.kt similarity index 100% rename from matrix-sdk-android/src/rustCrypto/java/org/matrix/android/sdk/internal/crypto/store/RustCryptoStore.kt rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/RustCryptoStore.kt diff --git a/matrix-sdk-android/src/rustCrypto/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStoreMigration.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStoreMigration.kt similarity index 100% rename from matrix-sdk-android/src/rustCrypto/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStoreMigration.kt rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStoreMigration.kt diff --git a/matrix-sdk-android/src/rustCrypto/java/org/matrix/android/sdk/internal/crypto/store/db/RustMigrationInfoProvider.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RustMigrationInfoProvider.kt similarity index 100% rename from matrix-sdk-android/src/rustCrypto/java/org/matrix/android/sdk/internal/crypto/store/db/RustMigrationInfoProvider.kt rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RustMigrationInfoProvider.kt diff --git a/matrix-sdk-android/src/rustCrypto/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo022.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo022.kt similarity index 100% rename from matrix-sdk-android/src/rustCrypto/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo022.kt rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo022.kt diff --git a/matrix-sdk-android/src/rustCrypto/java/org/matrix/android/sdk/internal/crypto/store/db/migration/rust/ExtractMigrationDataFailure.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/rust/ExtractMigrationDataFailure.kt similarity index 100% rename from matrix-sdk-android/src/rustCrypto/java/org/matrix/android/sdk/internal/crypto/store/db/migration/rust/ExtractMigrationDataFailure.kt rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/rust/ExtractMigrationDataFailure.kt diff --git a/matrix-sdk-android/src/rustCrypto/java/org/matrix/android/sdk/internal/crypto/store/db/migration/rust/ExtractMigrationDataUseCase.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/rust/ExtractMigrationDataUseCase.kt similarity index 100% rename from matrix-sdk-android/src/rustCrypto/java/org/matrix/android/sdk/internal/crypto/store/db/migration/rust/ExtractMigrationDataUseCase.kt rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/rust/ExtractMigrationDataUseCase.kt diff --git a/matrix-sdk-android/src/rustCrypto/java/org/matrix/android/sdk/internal/crypto/store/db/migration/rust/ExtractUtils.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/rust/ExtractUtils.kt similarity index 100% rename from matrix-sdk-android/src/rustCrypto/java/org/matrix/android/sdk/internal/crypto/store/db/migration/rust/ExtractUtils.kt rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/rust/ExtractUtils.kt diff --git a/matrix-sdk-android/src/rustCrypto/java/org/matrix/android/sdk/internal/crypto/verification/RustVerificationService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/RustVerificationService.kt similarity index 100% rename from matrix-sdk-android/src/rustCrypto/java/org/matrix/android/sdk/internal/crypto/verification/RustVerificationService.kt rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/RustVerificationService.kt diff --git a/matrix-sdk-android/src/rustCrypto/java/org/matrix/android/sdk/internal/crypto/verification/SasVerification.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/SasVerification.kt similarity index 100% rename from matrix-sdk-android/src/rustCrypto/java/org/matrix/android/sdk/internal/crypto/verification/SasVerification.kt rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/SasVerification.kt diff --git a/matrix-sdk-android/src/rustCrypto/java/org/matrix/android/sdk/internal/crypto/verification/VerificationListenersHolder.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationListenersHolder.kt similarity index 100% rename from matrix-sdk-android/src/rustCrypto/java/org/matrix/android/sdk/internal/crypto/verification/VerificationListenersHolder.kt rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationListenersHolder.kt diff --git a/matrix-sdk-android/src/rustCrypto/java/org/matrix/android/sdk/internal/crypto/verification/VerificationRequest.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationRequest.kt similarity index 100% rename from matrix-sdk-android/src/rustCrypto/java/org/matrix/android/sdk/internal/crypto/verification/VerificationRequest.kt rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationRequest.kt diff --git a/matrix-sdk-android/src/rustCrypto/java/org/matrix/android/sdk/internal/crypto/verification/VerificationsProvider.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationsProvider.kt similarity index 100% rename from matrix-sdk-android/src/rustCrypto/java/org/matrix/android/sdk/internal/crypto/verification/VerificationsProvider.kt rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationsProvider.kt diff --git a/matrix-sdk-android/src/rustCrypto/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/QrCodeVerification.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/QrCodeVerification.kt similarity index 100% rename from matrix-sdk-android/src/rustCrypto/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/QrCodeVerification.kt rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/QrCodeVerification.kt diff --git a/matrix-sdk-android/src/rustCrypto/java/org/matrix/android/sdk/internal/session/MigrateEAtoEROperation.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/MigrateEAtoEROperation.kt similarity index 100% rename from matrix-sdk-android/src/rustCrypto/java/org/matrix/android/sdk/internal/session/MigrateEAtoEROperation.kt rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/MigrateEAtoEROperation.kt diff --git a/matrix-sdk-android/src/rustCrypto/java/org/matrix/android/sdk/internal/session/SessionComponent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionComponent.kt similarity index 100% rename from matrix-sdk-android/src/rustCrypto/java/org/matrix/android/sdk/internal/session/SessionComponent.kt rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionComponent.kt diff --git a/matrix-sdk-android/src/rustCrypto/java/org/matrix/android/sdk/internal/session/sync/handler/ShieldSummaryUpdater.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/ShieldSummaryUpdater.kt similarity index 100% rename from matrix-sdk-android/src/rustCrypto/java/org/matrix/android/sdk/internal/session/sync/handler/ShieldSummaryUpdater.kt rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/ShieldSummaryUpdater.kt diff --git a/matrix-sdk-android/src/testKotlinCrypto/java/org/matrix/android/sdk/internal/crypto/UnRequestedKeysManagerTest.kt b/matrix-sdk-android/src/testKotlinCrypto/java/org/matrix/android/sdk/internal/crypto/UnRequestedKeysManagerTest.kt deleted file mode 100644 index 5b41ff6da0..0000000000 --- a/matrix-sdk-android/src/testKotlinCrypto/java/org/matrix/android/sdk/internal/crypto/UnRequestedKeysManagerTest.kt +++ /dev/null @@ -1,250 +0,0 @@ -/* - * Copyright 2022 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. - */ - -package org.matrix.android.sdk.internal.crypto - -import io.mockk.coEvery -import io.mockk.mockk -import kotlinx.coroutines.runBlocking -import org.amshove.kluent.fail -import org.amshove.kluent.shouldBe -import org.junit.Test -import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_MEGOLM -import org.matrix.android.sdk.api.session.crypto.crosssigning.DeviceTrustLevel -import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo -import org.matrix.android.sdk.api.session.crypto.model.ForwardedRoomKeyContent -import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap -import org.matrix.android.sdk.api.session.crypto.model.OlmDecryptionResult -import org.matrix.android.sdk.api.session.crypto.model.UnsignedDeviceInfo -import org.matrix.android.sdk.api.session.events.model.Event -import org.matrix.android.sdk.api.session.events.model.EventType -import org.matrix.android.sdk.api.session.events.model.content.OlmEventContent -import org.matrix.android.sdk.api.session.events.model.toContent -import org.matrix.android.sdk.internal.crypto.algorithms.megolm.UnRequestedForwardManager - -class UnRequestedKeysManagerTest { - - private val aliceMxId = "alice@example.com" - private val bobMxId = "bob@example.com" - private val bobDeviceId = "MKRJDSLYGA" - - private val device1Id = "MGDAADVDMG" - - private val aliceFirstDevice = CryptoDeviceInfo( - deviceId = device1Id, - userId = aliceMxId, - algorithms = MXCryptoAlgorithms.supportedAlgorithms(), - keys = mapOf( - "curve25519:$device1Id" to "yDa6cWOZ/WGBqm/JMUfTUCdEbAIzKHhuIcdDbnPEhDU", - "ed25519:$device1Id" to "XTge+TDwfm+WW10IGnaqEyLTSukPPzg3R1J1YvO1SBI", - ), - signatures = mapOf( - aliceMxId to mapOf( - "ed25519:$device1Id" - to "bPOAqM40+QSMgeEzUbYbPSZZccDDMUG00lCNdSXCoaS1gKKBGkSEaHO1OcibISIabjLYzmhp9mgtivz32fbABQ", - "ed25519:Ru4ni66dbQ6FZgUoHyyBtmjKecOHMvMSsSBZ2SABtt0" - to "owzUsQ4Pvn35uEIc5FdVnXVRPzsVYBV8uJRUSqr4y8r5tp0DvrMArtJukKETgYEAivcZMT1lwNihHIN9xh06DA" - ) - ), - unsigned = UnsignedDeviceInfo(deviceDisplayName = "Element Web"), - trustLevel = DeviceTrustLevel(crossSigningVerified = true, locallyVerified = true) - ) - - private val aBobDevice = CryptoDeviceInfo( - deviceId = bobDeviceId, - userId = bobMxId, - algorithms = MXCryptoAlgorithms.supportedAlgorithms(), - keys = mapOf( - "curve25519:$bobDeviceId" to "tWwg63Yfn//61Ylhir6z4QGejvo193E6MVHmURtYVn0", - "ed25519:$bobDeviceId" to "pS5NJ1LiVksQFX+p58NlphqMxE705laRVtUtZpYIAfs", - ), - signatures = mapOf( - bobMxId to mapOf( - "ed25519:$bobDeviceId" to "zAJqsmOSzkx8EWXcrynCsWtbgWZifN7A6DLyEBs+ZPPLnNuPN5Jwzc1Rg+oZWZaRPvOPcSL0cgcxRegSBU0NBA", - ) - ), - unsigned = UnsignedDeviceInfo(deviceDisplayName = "Element Ios") - ) - - @Test - fun `test process key request if invite received`() { - val fakeDeviceListManager = mockk { - coEvery { downloadKeys(any(), any()) } returns MXUsersDevicesMap().apply { - setObject(bobMxId, bobDeviceId, aBobDevice) - } - } - val unrequestedForwardManager = UnRequestedForwardManager(fakeDeviceListManager) - - val roomId = "someRoomId" - - unrequestedForwardManager.onUnRequestedKeyForward( - roomId, - createFakeSuccessfullyDecryptedForwardToDevice( - aBobDevice, - aliceFirstDevice, - aBobDevice, - megolmSessionId = "megolmId1" - ), - 1_000 - ) - - unrequestedForwardManager.onUnRequestedKeyForward( - roomId, - createFakeSuccessfullyDecryptedForwardToDevice( - aBobDevice, - aliceFirstDevice, - aBobDevice, - megolmSessionId = "megolmId2" - ), - 1_000 - ) - // for now no reason to accept - runBlocking { - unrequestedForwardManager.postSyncProcessParkedKeysIfNeeded(1000) { - fail("There should be no key to process") - } - } - - // ACT - // suppose an invite is received but from another user - val inviteTime = 1_000L - unrequestedForwardManager.onInviteReceived(roomId, "@jhon:example.com", inviteTime) - - // we shouldn't process the requests! -// runBlocking { - unrequestedForwardManager.postSyncProcessParkedKeysIfNeeded(inviteTime) { - fail("There should be no key to process") - } -// } - - // ACT - // suppose an invite is received from correct user - - unrequestedForwardManager.onInviteReceived(roomId, aBobDevice.userId, inviteTime) - runBlocking { - unrequestedForwardManager.postSyncProcessParkedKeysIfNeeded(inviteTime) { - it.size shouldBe 2 - } - } - } - - @Test - fun `test invite before keys`() { - val fakeDeviceListManager = mockk { - coEvery { downloadKeys(any(), any()) } returns MXUsersDevicesMap().apply { - setObject(bobMxId, bobDeviceId, aBobDevice) - } - } - val unrequestedForwardManager = UnRequestedForwardManager(fakeDeviceListManager) - - val roomId = "someRoomId" - - unrequestedForwardManager.onInviteReceived(roomId, aBobDevice.userId, 1_000) - - unrequestedForwardManager.onUnRequestedKeyForward( - roomId, - createFakeSuccessfullyDecryptedForwardToDevice( - aBobDevice, - aliceFirstDevice, - aBobDevice, - megolmSessionId = "megolmId1" - ), - 1_000 - ) - - runBlocking { - unrequestedForwardManager.postSyncProcessParkedKeysIfNeeded(1000) { - it.size shouldBe 1 - } - } - } - - @Test - fun `test validity window`() { - val fakeDeviceListManager = mockk { - coEvery { downloadKeys(any(), any()) } returns MXUsersDevicesMap().apply { - setObject(bobMxId, bobDeviceId, aBobDevice) - } - } - val unrequestedForwardManager = UnRequestedForwardManager(fakeDeviceListManager) - - val roomId = "someRoomId" - - val timeOfKeyReception = 1_000L - - unrequestedForwardManager.onUnRequestedKeyForward( - roomId, - createFakeSuccessfullyDecryptedForwardToDevice( - aBobDevice, - aliceFirstDevice, - aBobDevice, - megolmSessionId = "megolmId1" - ), - timeOfKeyReception - ) - - val currentTimeWindow = 10 * 60_000 - - // simulate very late invite - val inviteTime = timeOfKeyReception + currentTimeWindow + 1_000 - unrequestedForwardManager.onInviteReceived(roomId, aBobDevice.userId, inviteTime) - - runBlocking { - unrequestedForwardManager.postSyncProcessParkedKeysIfNeeded(inviteTime) { - fail("There should be no key to process") - } - } - } - - private fun createFakeSuccessfullyDecryptedForwardToDevice( - sentBy: CryptoDeviceInfo, - dest: CryptoDeviceInfo, - sessionInitiator: CryptoDeviceInfo, - algorithm: String = MXCRYPTO_ALGORITHM_MEGOLM, - roomId: String = "!zzgDlIhbWOevcdFBXr:example.com", - megolmSessionId: String = "Z/FSE8wDYheouGjGP9pezC4S1i39RtAXM3q9VXrBVZw" - ): Event { - return Event( - type = EventType.ENCRYPTED, - eventId = "!fake", - senderId = sentBy.userId, - content = OlmEventContent( - ciphertext = mapOf( - dest.identityKey()!! to mapOf( - "type" to 0, - "body" to "AwogcziNF/tv60X0elsBmnKPN3+LTXr4K3vXw+1ZJ6jpTxESIJCmMMDvOA+" - ) - ), - senderKey = sentBy.identityKey() - ).toContent(), - - ).apply { - mxDecryptionResult = OlmDecryptionResult( - payload = mapOf( - "type" to EventType.FORWARDED_ROOM_KEY, - "content" to ForwardedRoomKeyContent( - algorithm = algorithm, - roomId = roomId, - senderKey = sessionInitiator.identityKey(), - sessionId = megolmSessionId, - sessionKey = "AQAAAAAc4dK+lXxXyaFbckSxwjIEoIGDLKYovONJ7viWpwevhfvoBh+Q..." - ).toContent() - ), - senderKey = sentBy.identityKey() - ) - } - } -} diff --git a/matrix-sdk-android/src/testKotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/FakeCryptoStoreForVerification.kt b/matrix-sdk-android/src/testKotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/FakeCryptoStoreForVerification.kt deleted file mode 100644 index 493a5c13a9..0000000000 --- a/matrix-sdk-android/src/testKotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/FakeCryptoStoreForVerification.kt +++ /dev/null @@ -1,216 +0,0 @@ -/* - * Copyright 2022 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. - */ - -package org.matrix.android.sdk.internal.crypto.verification - -import io.mockk.coEvery -import io.mockk.every -import io.mockk.mockk -import org.matrix.android.sdk.api.session.crypto.crosssigning.CryptoCrossSigningKey -import org.matrix.android.sdk.api.session.crypto.crosssigning.DeviceTrustLevel -import org.matrix.android.sdk.api.session.crypto.crosssigning.KeyUsage -import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo -import org.matrix.android.sdk.api.session.crypto.model.UnsignedDeviceInfo -import org.matrix.android.sdk.internal.crypto.MXCryptoAlgorithms - -enum class StoreMode { - Alice, - Bob -} - -internal class FakeCryptoStoreForVerification(private val mode: StoreMode) { - - val instance = mockk() - - init { - every { instance.getMyDeviceId() } answers { - when (mode) { - StoreMode.Alice -> aliceDevice1Id - StoreMode.Bob -> bobDeviceId - } - } - - // order matters here but can't find any info in doc about that - every { instance.getUserDevice(any(), any()) } returns null - every { instance.getUserDevice(aliceMxId, aliceDevice1Id) } returns aliceFirstDevice - every { instance.getUserDevice(bobMxId, bobDeviceId) } returns aBobDevice - - every { instance.getUserDeviceList(aliceMxId) } returns listOf(aliceFirstDevice) - every { instance.getUserDeviceList(bobMxId) } returns listOf(aBobDevice) - coEvery { instance.locallyTrustDevice(any(), any()) } returns Unit - - coEvery { instance.getMyTrustedMasterKeyBase64() } answers { - when (mode) { - StoreMode.Alice -> { - aliceMSK - } - StoreMode.Bob -> { - bobMSK - } - } - } - - coEvery { instance.getUserMasterKeyBase64(any()) } answers { - val mxId = firstArg() - when (mxId) { - aliceMxId -> aliceMSK - bobMxId -> bobMSK - else -> null - } - } - - coEvery { instance.getMyDeviceId() } answers { - when (mode) { - StoreMode.Alice -> aliceDevice1Id - StoreMode.Bob -> bobDeviceId - } - } - - coEvery { instance.getMyDevice() } answers { - when (mode) { - StoreMode.Alice -> aliceFirstDevice - StoreMode.Bob -> aBobDevice - } - } - - coEvery { - instance.trustOwnDevice(any()) - } returns Unit - - coEvery { - instance.trustUser(any()) - } returns Unit - } - - companion object { - - val aliceMxId = "alice@example.com" - val bobMxId = "bob@example.com" - val bobDeviceId = "MKRJDSLYGA" - val bobDeviceId2 = "RRIWTEKZEI" - - val aliceDevice1Id = "MGDAADVDMG" - - private val aliceMSK = "Ru4ni66dbQ6FZgUoHyyBtmjKecOHMvMSsSBZ2SABtt0" - private val aliceSSK = "Rw6MiEn5do57mBWlWUvL6VDZJ7vAfGrTC58UXVyA0eo" - private val aliceUSK = "3XpDI8J5T1Wy2NoGePkDiVhqZlVeVPHM83q9sUJuRcc" - - private val bobMSK = "/ZK6paR+wBkKcazPx2xijn/0g+m2KCRqdCUZ6agzaaE" - private val bobSSK = "3/u3SRYywxRl2ul9OiRJK5zFeFnGXd0TrkcnVh1Bebk" - private val bobUSK = "601KhaiAhDTyFDS87leWc8/LB+EAUjKgjJvPMWNLP08" - - private val aliceFirstDevice = CryptoDeviceInfo( - deviceId = aliceDevice1Id, - userId = aliceMxId, - algorithms = MXCryptoAlgorithms.supportedAlgorithms(), - keys = mapOf( - "curve25519:$aliceDevice1Id" to "yDa6cWOZ/WGBqm/JMUfTUCdEbAIzKHhuIcdDbnPEhDU", - "ed25519:$aliceDevice1Id" to "XTge+TDwfm+WW10IGnaqEyLTSukPPzg3R1J1YvO1SBI", - ), - signatures = mapOf( - aliceMxId to mapOf( - "ed25519:$aliceDevice1Id" - to "bPOAqM40+QSMgeEzUbYbPSZZccDDMUG00lCNdSXCoaS1gKKBGkSEaHO1OcibISIabjLYzmhp9mgtivz32fbABQ", - "ed25519:$aliceMSK" - to "owzUsQ4Pvn35uEIc5FdVnXVRPzsVYBV8uJRUSqr4y8r5tp0DvrMArtJukKETgYEAivcZMT1lwNihHIN9xh06DA" - ) - ), - unsigned = UnsignedDeviceInfo(deviceDisplayName = "Element Web"), - trustLevel = DeviceTrustLevel(crossSigningVerified = true, locallyVerified = true) - ) - - private val aBobDevice = CryptoDeviceInfo( - deviceId = bobDeviceId, - userId = bobMxId, - algorithms = MXCryptoAlgorithms.supportedAlgorithms(), - keys = mapOf( - "curve25519:$bobDeviceId" to "tWwg63Yfn//61Ylhir6z4QGejvo193E6MVHmURtYVn0", - "ed25519:$bobDeviceId" to "pS5NJ1LiVksQFX+p58NlphqMxE705laRVtUtZpYIAfs", - ), - signatures = mapOf( - bobMxId to mapOf( - "ed25519:$bobDeviceId" to "zAJqsmOSzkx8EWXcrynCsWtbgWZifN7A6DLyEBs+ZPPLnNuPN5Jwzc1Rg+oZWZaRPvOPcSL0cgcxRegSBU0NBA", - ) - ), - unsigned = UnsignedDeviceInfo(deviceDisplayName = "Element Ios") - ) - - val aBobDevice2 = CryptoDeviceInfo( - deviceId = bobDeviceId2, - userId = bobMxId, - algorithms = MXCryptoAlgorithms.supportedAlgorithms(), - keys = mapOf( - "curve25519:$bobDeviceId" to "mE4WKAcyRRv7Gk1IDIVm0lZNzb8g9YL2eRQZUHmkkCI", - "ed25519:$bobDeviceId" to "yB/9LITHTqrvdXWDR2k6Qw/MDLUBWABlP9v2eYuqHPE", - ), - signatures = mapOf( - bobMxId to mapOf( - "ed25519:$bobDeviceId" to "zAJqsmOSzkx8EWXcrynCsWtbgWZifN7A6DLyEBs+ZPPLnNuPN5Jwzc1Rg+oZWZaRPvOPcSL0cgcxRegSBU0NBA", - ) - ), - unsigned = UnsignedDeviceInfo(deviceDisplayName = "Element Android") - ) - - private val aliceMSKBase = CryptoCrossSigningKey( - userId = aliceMxId, - usages = listOf(KeyUsage.MASTER.value), - keys = mapOf( - "ed25519$aliceMSK" to aliceMSK - ), - trustLevel = DeviceTrustLevel(true, true), - signatures = emptyMap() - ) - - private val aliceSSKBase = CryptoCrossSigningKey( - userId = aliceMxId, - usages = listOf(KeyUsage.SELF_SIGNING.value), - keys = mapOf( - "ed25519$aliceSSK" to aliceSSK - ), - trustLevel = null, - signatures = emptyMap() - ) - - private val aliceUSKBase = CryptoCrossSigningKey( - userId = aliceMxId, - usages = listOf(KeyUsage.USER_SIGNING.value), - keys = mapOf( - "ed25519$aliceUSK" to aliceUSK - ), - trustLevel = null, - signatures = emptyMap() - ) - - val bobMSKBase = aliceMSKBase.copy( - userId = bobMxId, - keys = mapOf( - "ed25519$bobMSK" to bobMSK - ), - ) - val bobUSKBase = aliceMSKBase.copy( - userId = bobMxId, - keys = mapOf( - "ed25519$bobUSK" to bobUSK - ), - ) - val bobSSKBase = aliceMSKBase.copy( - userId = bobMxId, - keys = mapOf( - "ed25519$bobSSK" to bobSSK - ), - ) - } -} diff --git a/matrix-sdk-android/src/testKotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/VerificationActorHelper.kt b/matrix-sdk-android/src/testKotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/VerificationActorHelper.kt deleted file mode 100644 index 49fd4a3fe2..0000000000 --- a/matrix-sdk-android/src/testKotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/VerificationActorHelper.kt +++ /dev/null @@ -1,284 +0,0 @@ -/* - * Copyright (c) 2022 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. - */ - -package org.matrix.android.sdk.internal.crypto.verification - -import io.mockk.coEvery -import io.mockk.every -import io.mockk.mockk -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.SupervisorJob -import kotlinx.coroutines.channels.SendChannel -import kotlinx.coroutines.launch -import org.matrix.android.sdk.api.session.crypto.verification.ValidVerificationInfoReady -import org.matrix.android.sdk.api.session.events.model.Content -import org.matrix.android.sdk.api.session.events.model.EventType -import org.matrix.android.sdk.api.session.events.model.toModel -import org.matrix.android.sdk.api.session.room.model.message.MessageVerificationReadyContent -import org.matrix.android.sdk.api.session.room.model.message.MessageVerificationRequestContent -import org.matrix.android.sdk.api.session.room.model.message.ValidVerificationDone -import java.util.UUID - -internal class VerificationActorHelper { - - data class TestData( - val aliceActor: VerificationActor, - val bobActor: VerificationActor, - val aliceStore: FakeCryptoStoreForVerification, - val bobStore: FakeCryptoStoreForVerification, - ) - - private val actorAScope = CoroutineScope(SupervisorJob()) - private val actorBScope = CoroutineScope(SupervisorJob()) - private val transportScope = CoroutineScope(SupervisorJob()) - - private var bobChannel: SendChannel? = null - private var aliceChannel: SendChannel? = null - - fun setUpActors(): TestData { - val aliceTransportLayer = mockTransportTo(FakeCryptoStoreForVerification.aliceMxId) { listOf(bobChannel) } - val bobTransportLayer = mockTransportTo(FakeCryptoStoreForVerification.bobMxId) { listOf(aliceChannel) } - - val fakeAliceStore = FakeCryptoStoreForVerification(StoreMode.Alice) - val aliceActor = fakeActor( - actorAScope, - FakeCryptoStoreForVerification.aliceMxId, - fakeAliceStore.instance, - aliceTransportLayer, - ) - aliceChannel = aliceActor.channel - - val fakeBobStore = FakeCryptoStoreForVerification(StoreMode.Bob) - val bobActor = fakeActor( - actorBScope, - FakeCryptoStoreForVerification.bobMxId, - fakeBobStore.instance, - bobTransportLayer - ) - bobChannel = bobActor.channel - - return TestData( - aliceActor = aliceActor, - bobActor = bobActor, - aliceStore = fakeAliceStore, - bobStore = fakeBobStore - ) - } - -// fun setupMultipleSessions() { -// val aliceTargetChannels = mutableListOf>() -// val aliceTransportLayer = mockTransportTo(FakeCryptoStoreForVerification.aliceMxId) { aliceTargetChannels } -// val bobTargetChannels = mutableListOf>() -// val bobTransportLayer = mockTransportTo(FakeCryptoStoreForVerification.bobMxId) { bobTargetChannels } -// val bob2TargetChannels = mutableListOf>() -// val bob2TransportLayer = mockTransportTo(FakeCryptoStoreForVerification.bobMxId) { bob2TargetChannels } -// -// val fakeAliceStore = FakeCryptoStoreForVerification(StoreMode.Alice) -// val aliceActor = fakeActor( -// actorAScope, -// FakeCryptoStoreForVerification.aliceMxId, -// fakeAliceStore.instance, -// aliceTransportLayer, -// ) -// -// val fakeBobStore1 = FakeCryptoStoreForVerification(StoreMode.Bob) -// val bobActor = fakeActor( -// actorBScope, -// FakeCryptoStoreForVerification.bobMxId, -// fakeBobStore1.instance, -// bobTransportLayer -// ) -// -// val actorCScope = CoroutineScope(SupervisorJob()) -// val fakeBobStore2 = FakeCryptoStoreForVerification(StoreMode.Bob) -// every { fakeBobStore2.instance.getMyDeviceId() } returns FakeCryptoStoreForVerification.bobDeviceId2 -// every { fakeBobStore2.instance.getMyDevice() } returns FakeCryptoStoreForVerification.aBobDevice2 -// -// val bobActor2 = fakeActor( -// actorCScope, -// FakeCryptoStoreForVerification.bobMxId, -// fakeBobStore2.instance, -// bobTransportLayer -// ) -// -// aliceTargetChannels.add(bobActor.channel) -// aliceTargetChannels.add(bobActor2.channel) -// -// bobTargetChannels.add(aliceActor.channel) -// bobTargetChannels.add(bobActor2.channel) -// -// bob2TargetChannels.add(aliceActor.channel) -// bob2TargetChannels.add(bobActor.channel) -// } - - private fun mockTransportTo(fromUser: String, otherChannel: (() -> List?>)): VerificationTransportLayer { - return mockk { - coEvery { sendToOther(any(), any(), any()) } answers { - val request = firstArg() - val type = secondArg() - val info = thirdArg>() - - transportScope.launch(Dispatchers.IO) { - when (type) { - EventType.KEY_VERIFICATION_READY -> { - val readyContent = info.asValidObject() - otherChannel().onEach { - it?.send( - VerificationIntent.OnReadyReceived( - transactionId = request.requestId, - fromUser = fromUser, - viaRoom = request.roomId, - readyInfo = readyContent as ValidVerificationInfoReady, - ) - ) - } - } - EventType.KEY_VERIFICATION_START -> { - val startContent = info.asValidObject() - otherChannel().onEach { - it?.send( - VerificationIntent.OnStartReceived( - fromUser = fromUser, - viaRoom = request.roomId, - validVerificationInfoStart = startContent as ValidVerificationInfoStart, - ) - ) - } - } - EventType.KEY_VERIFICATION_ACCEPT -> { - val content = info.asValidObject() - otherChannel().onEach { - it?.send( - VerificationIntent.OnAcceptReceived( - fromUser = fromUser, - viaRoom = request.roomId, - validAccept = content as ValidVerificationInfoAccept, - ) - ) - } - } - EventType.KEY_VERIFICATION_KEY -> { - val content = info.asValidObject() - otherChannel().onEach { - it?.send( - VerificationIntent.OnKeyReceived( - fromUser = fromUser, - viaRoom = request.roomId, - validKey = content as ValidVerificationInfoKey, - ) - ) - } - } - EventType.KEY_VERIFICATION_MAC -> { - val content = info.asValidObject() - otherChannel().onEach { - it?.send( - VerificationIntent.OnMacReceived( - fromUser = fromUser, - viaRoom = request.roomId, - validMac = content as ValidVerificationInfoMac, - ) - ) - } - } - EventType.KEY_VERIFICATION_DONE -> { - val content = info.asValidObject() - otherChannel().onEach { - it?.send( - VerificationIntent.OnDoneReceived( - fromUser = fromUser, - viaRoom = request.roomId, - transactionId = (content as ValidVerificationDone).transactionId, - ) - ) - } - } - } - } - } - coEvery { sendInRoom(any(), any(), any()) } answers { - val type = secondArg() - val roomId = thirdArg() - val content = arg(3) - - val fakeEventId = UUID.randomUUID().toString() - transportScope.launch(Dispatchers.IO) { - when (type) { - EventType.MESSAGE -> { - val requestContent = content.toModel()?.copy( - transactionId = fakeEventId - )?.asValidObject() - otherChannel().onEach { - it?.send( - VerificationIntent.OnVerificationRequestReceived( - requestContent!!, - senderId = FakeCryptoStoreForVerification.aliceMxId, - roomId = roomId, - timeStamp = 0 - ) - ) - } - } - EventType.KEY_VERIFICATION_READY -> { - val readyContent = content.toModel() - ?.asValidObject() - otherChannel().onEach { - it?.send( - VerificationIntent.OnReadyReceived( - transactionId = readyContent!!.transactionId, - fromUser = fromUser, - viaRoom = roomId, - readyInfo = readyContent, - ) - ) - } - } - } - } - fakeEventId - } - } - } - - private fun fakeActor( - scope: CoroutineScope, - userId: String, - cryptoStore: VerificationTrustBackend, - transportLayer: VerificationTransportLayer, - ): VerificationActor { - return VerificationActor( - scope, - clock = mockk { - every { epochMillis() } returns System.currentTimeMillis() - }, - myUserId = userId, - verificationTrustBackend = cryptoStore, - secretShareManager = mockk {}, - transportLayer = transportLayer, - verificationRequestsStore = VerificationRequestsStore(), - olmPrimitiveProvider = mockk { - every { provideOlmSas() } returns mockk { - every { publicKey } returns "Tm9JRGVhRmFrZQo=" - every { setTheirPublicKey(any()) } returns Unit - every { generateShortCode(any(), any()) } returns byteArrayOf(1, 2, 3, 4, 5, 6, 7, 8, 9) - every { calculateMac(any(), any()) } returns "mic mac mec" - } - every { sha256(any()) } returns "fake_hash" - } - ) - } -} diff --git a/matrix-sdk-android/src/testKotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/VerificationActorTest.kt b/matrix-sdk-android/src/testKotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/VerificationActorTest.kt deleted file mode 100644 index 364a3047ed..0000000000 --- a/matrix-sdk-android/src/testKotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/VerificationActorTest.kt +++ /dev/null @@ -1,528 +0,0 @@ -/* - * Copyright 2022 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. - */ - -package org.matrix.android.sdk.internal.crypto.verification.org.matrix.android.sdk.internal.crypto.verification - -import android.util.Base64 -import io.mockk.clearAllMocks -import io.mockk.coEvery -import io.mockk.coVerify -import io.mockk.every -import io.mockk.mockkConstructor -import io.mockk.mockkStatic -import kotlinx.coroutines.CompletableDeferred -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.SupervisorJob -import kotlinx.coroutines.cancel -import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.launch -import kotlinx.coroutines.test.runTest -import kotlinx.coroutines.withContext -import org.amshove.kluent.fail -import org.amshove.kluent.internal.assertEquals -import org.amshove.kluent.internal.assertNotEquals -import org.amshove.kluent.shouldBeEqualTo -import org.amshove.kluent.shouldNotBe -import org.amshove.kluent.shouldNotBeEqualTo -import org.json.JSONObject -import org.junit.After -import org.junit.Before -import org.junit.Test -import org.matrix.android.sdk.MatrixTest -import org.matrix.android.sdk.api.session.crypto.verification.EVerificationState -import org.matrix.android.sdk.api.session.crypto.verification.PendingVerificationRequest -import org.matrix.android.sdk.api.session.crypto.verification.SasTransactionState -import org.matrix.android.sdk.api.session.crypto.verification.SasVerificationTransaction -import org.matrix.android.sdk.api.session.crypto.verification.VerificationEvent -import org.matrix.android.sdk.api.session.crypto.verification.VerificationMethod -import org.matrix.android.sdk.api.session.crypto.verification.VerificationTransaction -import org.matrix.android.sdk.api.session.crypto.verification.getRequest -import org.matrix.android.sdk.internal.crypto.verification.FakeCryptoStoreForVerification -import org.matrix.android.sdk.internal.crypto.verification.VerificationActor -import org.matrix.android.sdk.internal.crypto.verification.VerificationActorHelper -import org.matrix.android.sdk.internal.crypto.verification.VerificationIntent - -@OptIn(ExperimentalCoroutinesApi::class) -class VerificationActorTest : MatrixTest { - - val transportScope = CoroutineScope(SupervisorJob()) - - @Before - fun setUp() { - // QR code needs that - mockkStatic(Base64::class) - every { - Base64.encodeToString(any(), any()) - } answers { - val array = firstArg() - java.util.Base64.getEncoder().encodeToString(array) - } - - every { - Base64.decode(any(), any()) - } answers { - val array = firstArg() - java.util.Base64.getDecoder().decode(array) - } - - // to mock canonical json - mockkConstructor(JSONObject::class) - every { anyConstructed().keys() } returns emptyList().listIterator() - -// mockkConstructor(KotlinSasTransaction::class) -// every { anyConstructed().getSAS() } returns mockk() { -// every { publicKey } returns "Tm9JRGVhRmFrZQo=" -// } - } - - @After - fun tearDown() { - clearAllMocks() - } - - @Test - fun `If ready both side should support sas and Qr show and scan`() = runTest { - val testData = VerificationActorHelper().setUpActors() - val aliceActor = testData.aliceActor - val bobActor = testData.bobActor - - val completableDeferred = CompletableDeferred() - - transportScope.launch { - bobActor.eventFlow.collect { - if (it is VerificationEvent.RequestAdded) { - completableDeferred.complete(it.request) - return@collect cancel() - } - } - } - - aliceActor.requestVerification(listOf(VerificationMethod.SAS, VerificationMethod.QR_CODE_SHOW, VerificationMethod.QR_CODE_SCAN)) - - val bobIncomingRequest = completableDeferred.await() - bobIncomingRequest.state shouldBeEqualTo EVerificationState.Requested - - val aliceReadied = CompletableDeferred() - - transportScope.launch { - aliceActor.eventFlow.collect { - if (it is VerificationEvent.RequestUpdated && it.request.state == EVerificationState.Ready) { - aliceReadied.complete(it.request) - return@collect cancel() - } - } - } - - // test ready - val bobReadied = awaitDeferrable { - bobActor.send( - VerificationIntent.ActionReadyRequest( - bobIncomingRequest.transactionId, - methods = listOf(VerificationMethod.SAS, VerificationMethod.QR_CODE_SHOW, VerificationMethod.QR_CODE_SCAN), - it - ) - ) - } - - val readiedAliceSide = aliceReadied.await() - - readiedAliceSide.isSasSupported shouldBeEqualTo true - readiedAliceSide.weShouldDisplayQRCode shouldBeEqualTo true - - bobReadied shouldNotBe null - bobReadied!!.isSasSupported shouldBeEqualTo true - bobReadied.weShouldDisplayQRCode shouldBeEqualTo true - - bobReadied.qrCodeText shouldNotBe null - readiedAliceSide.qrCodeText shouldNotBe null - } - - @Test - fun `Test alice can show but not scan QR`() = runTest { - val testData = VerificationActorHelper().setUpActors() - val aliceActor = testData.aliceActor - val bobActor = testData.bobActor - - println("Alice sends a request") - val outgoingRequest = aliceActor.requestVerification( - listOf(VerificationMethod.SAS, VerificationMethod.QR_CODE_SHOW) - ) - - // wait for bob to get it - println("Wait for bob to get it") - waitForBobToSeeIncomingRequest(bobActor, outgoingRequest) - - println("let bob ready it") - val bobReady = bobActor.readyVerification( - outgoingRequest.transactionId, - listOf(VerificationMethod.SAS, VerificationMethod.QR_CODE_SCAN, VerificationMethod.QR_CODE_SHOW) - ) - - println("Wait for alice to get the ready") - retryUntil { - awaitDeferrable { - aliceActor.send(VerificationIntent.GetExistingRequest(outgoingRequest.transactionId, outgoingRequest.otherUserId, it)) - }?.state == EVerificationState.Ready - } - - val aliceReady = awaitDeferrable { - aliceActor.send(VerificationIntent.GetExistingRequest(outgoingRequest.transactionId, outgoingRequest.otherUserId, it)) - }!! - - aliceReady.isSasSupported shouldBeEqualTo bobReady.isSasSupported - - // alice can't scan so there should not be option to do so - assertEquals("Alice should not show scan option", false, aliceReady.weShouldShowScanOption) - assertEquals("Alice should show QR as bob can scan", true, aliceReady.weShouldDisplayQRCode) - - assertEquals("Bob should be able to scan", true, bobReady.weShouldShowScanOption) - assertEquals("Bob should not show QR as alice can scan", false, bobReady.weShouldDisplayQRCode) - } - - @Test - fun `If Bobs device does not understand any of the methods, it should not cancel the request`() = runTest { - val testData = VerificationActorHelper().setUpActors() - val aliceActor = testData.aliceActor - val bobActor = testData.bobActor - - val outgoingRequest = aliceActor.requestVerification( - listOf(VerificationMethod.SAS) - ) - - // wait for bob to get it - waitForBobToSeeIncomingRequest(bobActor, outgoingRequest) - - println("let bob ready it") - try { - bobActor.readyVerification( - outgoingRequest.transactionId, - listOf(VerificationMethod.QR_CODE_SCAN, VerificationMethod.QR_CODE_SHOW) - ) - // Upon receipt of Alice’s m.key.verification.request message, if Bob’s device does not understand any of the methods, - // it should not cancel the request as one of his other devices may support the request - fail("Ready should fail as no common methods") - } catch (failure: Throwable) { - // should throw - } - - val bodSide = awaitDeferrable { - bobActor.send(VerificationIntent.GetExistingRequest(outgoingRequest.transactionId, FakeCryptoStoreForVerification.aliceMxId, it)) - }!! - - bodSide.state shouldNotBeEqualTo EVerificationState.Cancelled - } - - @Test - fun `Test bob can show but not scan QR`() = runTest { - val testData = VerificationActorHelper().setUpActors() - val aliceActor = testData.aliceActor - val bobActor = testData.bobActor - - println("Alice sends a request") - val outgoingRequest = aliceActor.requestVerification( - listOf(VerificationMethod.QR_CODE_SCAN, VerificationMethod.QR_CODE_SHOW) - ) - - // wait for bob to get it - println("Wait for bob to get it") - waitForBobToSeeIncomingRequest(bobActor, outgoingRequest) - - println("let bob ready it") - val bobReady = bobActor.readyVerification( - outgoingRequest.transactionId, - listOf(VerificationMethod.QR_CODE_SHOW) - ) - - println("Wait for alice to get the ready") - retryUntil { - awaitDeferrable { - aliceActor.send(VerificationIntent.GetExistingRequest(outgoingRequest.transactionId, outgoingRequest.otherUserId, it)) - }?.state == EVerificationState.Ready - } - - val aliceReady = awaitDeferrable { - aliceActor.send(VerificationIntent.GetExistingRequest(outgoingRequest.transactionId, outgoingRequest.otherUserId, it)) - }!! - - assertEquals("Alice sas is not supported", false, aliceReady.isSasSupported) - aliceReady.isSasSupported shouldBeEqualTo bobReady.isSasSupported - - // alice can't scan so there should not be option to do so - assertEquals("Alice should show scan option", true, aliceReady.weShouldShowScanOption) - assertEquals("Alice QR data should be null", null, aliceReady.qrCodeText) - assertEquals("Alice should not show QR as bob can scan", false, aliceReady.weShouldDisplayQRCode) - - assertEquals("Bob should not should not show cam option as it can't scan", false, bobReady.weShouldShowScanOption) - assertNotEquals("Bob QR data should be there", null, bobReady.qrCodeText) - assertEquals("Bob should show QR as alice can scan", true, bobReady.weShouldDisplayQRCode) - } - - @Test - fun `Test verify to users that trust their cross signing keys`() = runTest { - val testData = VerificationActorHelper().setUpActors() - val aliceActor = testData.aliceActor - val bobActor = testData.bobActor - - coEvery { testData.bobStore.instance.canCrossSign() } returns true - coEvery { testData.aliceStore.instance.canCrossSign() } returns true - - fullSasVerification(bobActor, aliceActor) - - coVerify { - testData.bobStore.instance.locallyTrustDevice( - FakeCryptoStoreForVerification.aliceMxId, - FakeCryptoStoreForVerification.aliceDevice1Id, - ) - } - - coVerify { - testData.bobStore.instance.trustUser( - FakeCryptoStoreForVerification.aliceMxId - ) - } - - coVerify { - testData.aliceStore.instance.locallyTrustDevice( - FakeCryptoStoreForVerification.bobMxId, - FakeCryptoStoreForVerification.bobDeviceId, - ) - } - - coVerify { - testData.aliceStore.instance.trustUser( - FakeCryptoStoreForVerification.bobMxId - ) - } - } - - @Test - fun `Test user verification when alice do not trust her keys`() = runTest { - val testData = VerificationActorHelper().setUpActors() - val aliceActor = testData.aliceActor - val bobActor = testData.bobActor - - coEvery { testData.bobStore.instance.canCrossSign() } returns true - coEvery { testData.aliceStore.instance.canCrossSign() } returns false - coEvery { testData.aliceStore.instance.getMyTrustedMasterKeyBase64() } returns null - - fullSasVerification(bobActor, aliceActor) - - coVerify { - testData.bobStore.instance.locallyTrustDevice( - FakeCryptoStoreForVerification.aliceMxId, - FakeCryptoStoreForVerification.aliceDevice1Id, - ) - } - - coVerify(exactly = 0) { - testData.bobStore.instance.trustUser( - FakeCryptoStoreForVerification.aliceMxId - ) - } - - coVerify { - testData.aliceStore.instance.locallyTrustDevice( - FakeCryptoStoreForVerification.bobMxId, - FakeCryptoStoreForVerification.bobDeviceId, - ) - } - - coVerify(exactly = 0) { - testData.aliceStore.instance.trustUser( - FakeCryptoStoreForVerification.bobMxId - ) - } - } - - private suspend fun fullSasVerification(bobActor: VerificationActor, aliceActor: VerificationActor) { - transportScope.launch { - bobActor.eventFlow - .collect { - println("Bob flow 1 event $it") - if (it is VerificationEvent.RequestAdded) { - // auto accept - bobActor.readyVerification( - it.transactionId, - listOf(VerificationMethod.SAS) - ) - // then start - bobActor.send( - VerificationIntent.ActionStartSasVerification( - FakeCryptoStoreForVerification.aliceMxId, - it.transactionId, - CompletableDeferred() - ) - ) - } - return@collect cancel() - } - } - - val aliceCode = CompletableDeferred() - val bobCode = CompletableDeferred() - - aliceActor.eventFlow.onEach { - println("Alice flow event $it") - if (it is VerificationEvent.TransactionUpdated) { - val sasVerificationTransaction = it.transaction as SasVerificationTransaction - if (sasVerificationTransaction.state() is SasTransactionState.SasShortCodeReady) { - aliceCode.complete(sasVerificationTransaction) - } - } - }.launchIn(transportScope) - - bobActor.eventFlow.onEach { - println("Bob flow event $it") - if (it is VerificationEvent.TransactionUpdated) { - val sasVerificationTransaction = it.transaction as SasVerificationTransaction - if (sasVerificationTransaction.state() is SasTransactionState.SasShortCodeReady) { - bobCode.complete(sasVerificationTransaction) - } - } - }.launchIn(transportScope) - - println("Alice sends a request") - val outgoingRequest = aliceActor.requestVerification( - listOf(VerificationMethod.SAS) - ) - - // asserting the code won't help much here as all is mocked - // we are checking state progression - // Both transaction should be in sas ready - val aliceCodeReadyTx = aliceCode.await() - bobCode.await() - - // If alice accept the code, bob should pass to state mac received but code not comfirmed - aliceCodeReadyTx.userHasVerifiedShortCode() - - retryUntil { - val tx = bobActor.getTransactionBobPov(outgoingRequest.transactionId) - val sasTx = tx as? SasVerificationTransaction - val state = sasTx?.state() - (state is SasTransactionState.SasMacReceived && !state.codeConfirmed) - } - - val bobTransaction = bobActor.getTransactionBobPov(outgoingRequest.transactionId) as SasVerificationTransaction - - val bobDone = CompletableDeferred(Unit) - val aliceDone = CompletableDeferred(Unit) - transportScope.launch { - bobActor.eventFlow - .collect { - println("Bob flow 1 event $it") - it.getRequest()?.let { - if (it.transactionId == outgoingRequest.transactionId && it.state == EVerificationState.Done) { - bobDone.complete(Unit) - return@collect cancel() - } - } - } - } - transportScope.launch { - aliceActor.eventFlow - .collect { - println("Bob flow 1 event $it") - it.getRequest()?.let { - if (it.transactionId == outgoingRequest.transactionId && it.state == EVerificationState.Done) { - bobDone.complete(Unit) - return@collect cancel() - } - } - } - } - - // mark as verified from bob side - bobTransaction.userHasVerifiedShortCode() - - aliceDone.await() - bobDone.await() - } - - internal suspend fun VerificationActor.getTransactionBobPov(transactionId: String): VerificationTransaction? { - return awaitDeferrable { - channel.send( - VerificationIntent.GetExistingTransaction( - transactionId = transactionId, - fromUser = FakeCryptoStoreForVerification.aliceMxId, - it - ) - ) - } - } - - private suspend fun VerificationActor.requestVerification(methods: List): PendingVerificationRequest { - return awaitDeferrable { - send( - VerificationIntent.ActionRequestVerification( - otherUserId = FakeCryptoStoreForVerification.bobMxId, - roomId = "aRoom", - methods = methods, - deferred = it - ) - ) - } - } - - private suspend fun waitForBobToSeeIncomingRequest(bobActor: VerificationActor, aliceOutgoing: PendingVerificationRequest) { - retryUntil { - awaitDeferrable { - bobActor.send( - VerificationIntent.GetExistingRequest( - aliceOutgoing.transactionId, - FakeCryptoStoreForVerification.aliceMxId, it - ) - ) - }?.state == EVerificationState.Requested - } - } - - private val backoff = listOf(500L, 1_000L, 1_000L, 3_000L, 3_000L, 5_000L) - - private suspend fun retryUntil(condition: suspend (() -> Boolean)) { - var tryCount = 0 - while (!condition()) { - if (tryCount >= backoff.size) { - fail("Retry Until Fialed") - } - withContext(Dispatchers.IO) { - delay(backoff[tryCount]) - } - tryCount++ - } - } - - private suspend fun awaitDeferrable(block: suspend ((CompletableDeferred) -> Unit)): T { - val deferred = CompletableDeferred() - block.invoke(deferred) - return deferred.await() - } - - private suspend fun VerificationActor.readyVerification(transactionId: String, methods: List): PendingVerificationRequest { - return awaitDeferrable { - send( - VerificationIntent.ActionReadyRequest( - transactionId, - methods = methods, - it - ) - ) - }!! - } -} diff --git a/vector-app/build.gradle b/vector-app/build.gradle index 9aff3eaa84..5bd2843c03 100644 --- a/vector-app/build.gradle +++ b/vector-app/build.gradle @@ -67,10 +67,8 @@ def getVersionCode() { def getNightlyUniversalApkPath() { def taskNames = gradle.getStartParameter().taskNames.toString() - if(taskNames.contains("RustCryptoNightly")) { - return "vector-app/build/outputs/apk/gplayRustCrypto/nightly/vector-gplay-rustCrypto-universal-nightly.apk" - } else if (taskNames.contains("KotlinCryptoNightly")) { - return "vector-app/build/outputs/apk/gplayKotlinCrypto/nightly/vector-gplay-kotlinCrypto-universal-nightly.apk" + if(taskNames.contains("Nightly")) { + return "vector-app/build/outputs/apk/gplay/nightly/vector-gplay-universal-nightly.apk" } else { return "" } @@ -309,7 +307,7 @@ android { } } - flavorDimensions "store", "crypto" + flavorDimensions "store" productFlavors { gplay { @@ -332,23 +330,6 @@ android { buildConfigField "String", "FLAVOR_DESCRIPTION", "\"FDroid\"" } - kotlinCrypto { - dimension "crypto" - // versionName "${versionMajor}.${versionMinor}.${versionPatch}${getFdroidVersionSuffix()}" - buildConfigField "String", "CRYPTO_FLAVOR_DESCRIPTION", "\"olm-crypto\"" -// buildConfigField "String", "FLAVOR_DESCRIPTION", "\"KotlinCrypto\"" - } - rustCrypto { - dimension "crypto" - isDefault = true - // applicationIdSuffix ".corroded" - // versionNameSuffix "-R" - // resValue "string", "app_name", "ER" - -// // versionName "${versionMajor}.${versionMinor}.${versionPatch}${getFdroidVersionSuffix()}" - buildConfigField "String", "CRYPTO_FLAVOR_DESCRIPTION", "\"rust-crypto\"" -// buildConfigField "String", "FLAVOR_DESCRIPTION", "\"RustCrypto\"" - } } variantFilter { variant -> diff --git a/vector-app/src/fdroidRustCryptoNightly/res/values/colors.xml b/vector-app/src/fdroidRustCryptoNightly/res/values/colors.xml deleted file mode 100644 index 2ec78e4096..0000000000 --- a/vector-app/src/fdroidRustCryptoNightly/res/values/colors.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - #FF5964 - diff --git a/vector-app/src/main/java/im/vector/app/core/di/SingletonModule.kt b/vector-app/src/main/java/im/vector/app/core/di/SingletonModule.kt index 2a917593d8..fef6a54a3c 100644 --- a/vector-app/src/main/java/im/vector/app/core/di/SingletonModule.kt +++ b/vector-app/src/main/java/im/vector/app/core/di/SingletonModule.kt @@ -148,7 +148,6 @@ import javax.inject.Singleton ): MatrixConfiguration { return MatrixConfiguration( applicationFlavor = BuildConfig.FLAVOR_DESCRIPTION, - cryptoFlavor = BuildConfig.CRYPTO_FLAVOR_DESCRIPTION, roomDisplayNameFallbackProvider = vectorRoomDisplayNameFallbackProvider, threadMessagesEnabledDefault = vectorPreferences.areThreadMessagesEnabled(), networkInterceptors = listOfNotNull( diff --git a/vector/build.gradle b/vector/build.gradle index e45d23a11e..d6520edf1d 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -107,8 +107,6 @@ android { } } -apply from: '../flavor.gradle' - dependencies { implementation project(":vector-config") diff --git a/vector/src/main/java/im/vector/app/core/di/ConfigurationModule.kt b/vector/src/main/java/im/vector/app/core/di/ConfigurationModule.kt index 7c8dea3b7d..67559a613d 100644 --- a/vector/src/main/java/im/vector/app/core/di/ConfigurationModule.kt +++ b/vector/src/main/java/im/vector/app/core/di/ConfigurationModule.kt @@ -38,21 +38,9 @@ object ConfigurationModule { @Provides fun providesAnalyticsConfig(): AnalyticsConfig { val config: Analytics = when (BuildConfig.BUILD_TYPE) { - "debug" -> if (BuildConfig.FLAVOR == "rustCrypto") { - Config.ER_DEBUG_ANALYTICS_CONFIG - } else { - Config.DEBUG_ANALYTICS_CONFIG - } - "nightly" -> if (BuildConfig.FLAVOR == "rustCrypto") { - Config.ER_NIGHTLY_ANALYTICS_CONFIG - } else { - Config.NIGHTLY_ANALYTICS_CONFIG - } - "release" -> if (BuildConfig.FLAVOR == "rustCrypto") { - Config.RELEASE_R_ANALYTICS_CONFIG - } else { - Config.RELEASE_ANALYTICS_CONFIG - } + "debug" -> Config.DEBUG_ANALYTICS_CONFIG + "nightly" -> Config.NIGHTLY_ANALYTICS_CONFIG + "release" -> Config.RELEASE_ANALYTICS_CONFIG else -> throw IllegalStateException("Unhandled build type: ${BuildConfig.BUILD_TYPE}") } return when (config) { diff --git a/vector/src/main/java/im/vector/app/features/MainActivity.kt b/vector/src/main/java/im/vector/app/features/MainActivity.kt index 48fe092524..c8aa108ccb 100644 --- a/vector/src/main/java/im/vector/app/features/MainActivity.kt +++ b/vector/src/main/java/im/vector/app/features/MainActivity.kt @@ -29,7 +29,6 @@ import com.airbnb.mvrx.viewModel import com.bumptech.glide.Glide import com.google.android.material.dialog.MaterialAlertDialogBuilder import dagger.hilt.android.AndroidEntryPoint -import im.vector.app.BuildConfig import im.vector.app.R import im.vector.app.core.extensions.startSyncing import im.vector.app.core.extensions.vectorStore @@ -181,9 +180,7 @@ class MainActivity : VectorBaseActivity(), UnlockedActivity } } - if (BuildConfig.FLAVOR == "rustCrypto") { - vectorPreferences.setIsOnRustCrypto(true) - } + vectorPreferences.setIsOnRustCrypto(true) if (intent.hasExtra(EXTRA_NEXT_INTENT)) { // Start the next Activity diff --git a/vector/src/main/java/im/vector/app/features/analytics/impl/DefaultVectorAnalytics.kt b/vector/src/main/java/im/vector/app/features/analytics/impl/DefaultVectorAnalytics.kt index d70dba773a..78a87b38b0 100644 --- a/vector/src/main/java/im/vector/app/features/analytics/impl/DefaultVectorAnalytics.kt +++ b/vector/src/main/java/im/vector/app/features/analytics/impl/DefaultVectorAnalytics.kt @@ -19,7 +19,6 @@ package im.vector.app.features.analytics.impl import com.posthog.android.Options import com.posthog.android.PostHog import com.posthog.android.Properties -import im.vector.app.BuildConfig import im.vector.app.core.di.NamedGlobalScope import im.vector.app.features.analytics.AnalyticsConfig import im.vector.app.features.analytics.VectorAnalytics @@ -215,9 +214,7 @@ class DefaultVectorAnalytics @Inject constructor( private fun Map.toPostHogUserProperties(): Properties { return Properties().apply { putAll(this@toPostHogUserProperties.filter { it.value != null }) - if (BuildConfig.FLAVOR == "rustCrypto") { - put("crypto", "rust") - } + put("crypto", "rust") } } diff --git a/vector/src/main/java/im/vector/app/features/analytics/metrics/sentry/SentryCryptoAnalytics.kt b/vector/src/main/java/im/vector/app/features/analytics/metrics/sentry/SentryCryptoAnalytics.kt index 2fcbc5fc56..2a904712dc 100644 --- a/vector/src/main/java/im/vector/app/features/analytics/metrics/sentry/SentryCryptoAnalytics.kt +++ b/vector/src/main/java/im/vector/app/features/analytics/metrics/sentry/SentryCryptoAnalytics.kt @@ -16,7 +16,6 @@ package im.vector.app.features.analytics.metrics.sentry -import im.vector.app.BuildConfig import io.sentry.Sentry import io.sentry.SentryEvent import io.sentry.protocol.Message @@ -29,7 +28,7 @@ class SentryCryptoAnalytics @Inject constructor() : CryptoMetricPlugin() { override fun captureEvent(cryptoEvent: CryptoEvent) { if (!Sentry.isEnabled()) return val event = SentryEvent() - event.setTag("e2eFlavor", BuildConfig.FLAVOR) + event.setTag("e2eFlavor", "rustCrypto") event.setTag("e2eType", "crypto") when (cryptoEvent) { is CryptoEvent.FailedToDecryptToDevice -> { diff --git a/vector/src/main/java/im/vector/app/features/crypto/keysbackup/restore/KeysBackupRestoreFromKeyViewModel.kt b/vector/src/main/java/im/vector/app/features/crypto/keysbackup/restore/KeysBackupRestoreFromKeyViewModel.kt index 5fd5f8cab8..539a9e746e 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/keysbackup/restore/KeysBackupRestoreFromKeyViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/keysbackup/restore/KeysBackupRestoreFromKeyViewModel.kt @@ -45,7 +45,7 @@ class KeysBackupRestoreFromKeyViewModel @Inject constructor( viewModelScope.launch(Dispatchers.IO) { try { val recoveryKey = BackupUtils.recoveryKeyFromBase58(recoveryCode.value!!) - sharedViewModel.recoverUsingBackupRecoveryKey(recoveryKey!!) + sharedViewModel.recoverUsingBackupRecoveryKey(recoveryKey) } catch (failure: Throwable) { recoveryCodeErrorText.postValue(stringProvider.getString(R.string.keys_backup_recovery_code_error_decrypt)) } diff --git a/vector/src/main/java/im/vector/app/features/crypto/keysbackup/restore/KeysBackupRestoreSharedViewModel.kt b/vector/src/main/java/im/vector/app/features/crypto/keysbackup/restore/KeysBackupRestoreSharedViewModel.kt index 004600edfd..46ef811807 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/keysbackup/restore/KeysBackupRestoreSharedViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/keysbackup/restore/KeysBackupRestoreSharedViewModel.kt @@ -226,7 +226,7 @@ class KeysBackupRestoreSharedViewModel @Inject constructor( try { val computedRecoveryKey = computeRecoveryKey(secret.fromBase64()) val backupRecoveryKey = BackupUtils.recoveryKeyFromBase58(computedRecoveryKey) - recoverUsingBackupRecoveryKey(backupRecoveryKey!!) + recoverUsingBackupRecoveryKey(backupRecoveryKey) } catch (failure: Throwable) { _navigateEvent.postValue( LiveEvent(NAVIGATE_FAILED_TO_LOAD_4S) diff --git a/vector/src/main/java/im/vector/app/features/crypto/recover/BackupToQuadSMigrationTask.kt b/vector/src/main/java/im/vector/app/features/crypto/recover/BackupToQuadSMigrationTask.kt index 9948ea4b3a..94dba48120 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/recover/BackupToQuadSMigrationTask.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/recover/BackupToQuadSMigrationTask.kt @@ -93,8 +93,7 @@ class BackupToQuadSMigrationTask @Inject constructor( reportProgress(params, R.string.bootstrap_progress_compute_curve_key) val recoveryKey = computeRecoveryKey(curveKey) val backupRecoveryKey = BackupUtils.recoveryKeyFromBase58(recoveryKey) - val isValid = backupRecoveryKey?.let { keysBackupService.isValidRecoveryKeyForCurrentVersion(it) } - ?: false + val isValid = backupRecoveryKey.let { keysBackupService.isValidRecoveryKeyForCurrentVersion(it) } if (!isValid) return Result.InvalidRecoverySecret diff --git a/vector/src/main/java/im/vector/app/features/crypto/verification/self/SelfVerificationViewModel.kt b/vector/src/main/java/im/vector/app/features/crypto/verification/self/SelfVerificationViewModel.kt index 55d84a2446..029fa2ca15 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/verification/self/SelfVerificationViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/verification/self/SelfVerificationViewModel.kt @@ -535,8 +535,7 @@ class SelfVerificationViewModel @AssistedInject constructor( val recoveryKey = computeRecoveryKey(secret.fromBase64()) val backupRecoveryKey = BackupUtils.recoveryKeyFromBase58(recoveryKey) val isValid = backupRecoveryKey - ?.let { session.cryptoService().keysBackupService().isValidRecoveryKeyForCurrentVersion(it) } - ?: false + .let { session.cryptoService().keysBackupService().isValidRecoveryKeyForCurrentVersion(it) } if (isValid) { session.cryptoService().keysBackupService().saveBackupRecoveryKey(backupRecoveryKey, version.version) // session.cryptoService().keysBackupService().trustKeysBackupVersion(version, true) From df82eee736b04460253abbb023181f598418a071 Mon Sep 17 00:00:00 2001 From: Valere Date: Mon, 2 Oct 2023 14:37:15 +0200 Subject: [PATCH 03/61] Code quality kdocs --- .../crypto/keysbackup/BackupRecoveryKey.kt | 6 ++++- .../android/sdk/internal/crypto/Device.kt | 11 +++++---- .../android/sdk/internal/crypto/OlmMachine.kt | 15 ++++++++---- .../crypto/PrepareToEncryptUseCase.kt | 3 ++- .../crypto/RustCrossSigningService.kt | 14 +++++------ .../crypto/keysbackup/RustKeyBackupService.kt | 4 ++-- .../internal/crypto/store/RustCryptoStore.kt | 5 ++-- .../{ExtractUtils.kt => RealmToMigrate.kt} | 0 .../verification/RustVerificationService.kt | 12 ++++++---- .../crypto/verification/SasVerification.kt | 24 +++++++++---------- .../verification/qrcode/QrCodeVerification.kt | 24 +++++++++---------- 11 files changed, 67 insertions(+), 51 deletions(-) rename matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/rust/{ExtractUtils.kt => RealmToMigrate.kt} (100%) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/keysbackup/BackupRecoveryKey.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/keysbackup/BackupRecoveryKey.kt index a3ab09d3d6..48db9c5ebf 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/keysbackup/BackupRecoveryKey.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/keysbackup/BackupRecoveryKey.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 The Matrix.org Foundation C.I.C. + * Copyright (c) 2023 New Vector Ltd * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -50,6 +50,10 @@ class BackupRecoveryKey internal constructor(internal val inner: InnerBackupReco return this.toBase58() == other.toBase58() } + override fun hashCode(): Int { + return toBase58().hashCode() + } + override fun toBase58() = inner.toBase58() override fun toBase64() = inner.toBase64() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/Device.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/Device.kt index 4cb329175b..0bd6ed06d1 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/Device.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/Device.kt @@ -68,7 +68,7 @@ internal class Device @AssistedInject constructor( } /** - * Request an interactive verification to begin + * Request an interactive verification to begin. * * This sends out a m.key.verification.request event over to-device messaging to * to this device. @@ -97,7 +97,8 @@ internal class Device @AssistedInject constructor( } } - /** Start an interactive verification with this device + /** + * Start an interactive verification with this device. * * This sends out a m.key.verification.start event with the method set to * m.sas.v1 to this device using to-device messaging. @@ -126,7 +127,7 @@ internal class Device @AssistedInject constructor( } /** - * Mark this device as locally trusted + * Mark this device as locally trusted. * * This won't upload any signatures, it will only mark the device as trusted * in the local database. @@ -139,7 +140,7 @@ internal class Device @AssistedInject constructor( } /** - * Manually verify this device + * Manually verify this device. * * This will sign the device with our self-signing key and upload the signatures * to the server. @@ -157,7 +158,7 @@ internal class Device @AssistedInject constructor( } /** - * Get the DeviceTrustLevel of this device + * Get the DeviceTrustLevel of this device. */ @Throws(CryptoStoreException::class) suspend fun trustLevel(): DeviceTrustLevel { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OlmMachine.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OlmMachine.kt index 4646d74c9a..998c6ef753 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OlmMachine.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OlmMachine.kt @@ -251,6 +251,10 @@ internal class OlmMachine @Inject constructor( * sync. * * @param keyCounts The map of uploaded one-time key types and counts. + * + * @param deviceUnusedFallbackKeyTypes The key algorithms for which the server has an unused fallback key for the device. + * + * @return The handled events, decrypted if needed (secrets are zeroised). */ @Throws(CryptoStoreException::class) suspend fun receiveSyncChanges( @@ -312,7 +316,7 @@ internal class OlmMachine @Inject constructor( } /** - * Used for lazy migration of inboundGroupSession from EA to ER + * Used for lazy migration of inboundGroupSession from EA to ER. */ suspend fun importRoomKey(inbound: MXInboundMegolmSessionWrapper): Result { Timber.v("Migration:: Tentative lazy migration") @@ -380,6 +384,8 @@ internal class OlmMachine @Inject constructor( * @param users The list of users which are considered to be members of the room and should * receive the room key. * + * @param settings The encryption settings for that room. + * * @return The list of [Request.ToDevice] that need to be sent out. */ @Throws(CryptoStoreException::class) @@ -723,7 +729,7 @@ internal class OlmMachine @Inject constructor( ensureUsersKeys.invoke(userIds, forceDownload) } - fun getUserIdentityFlow(userId: String): Flow> { + private fun getUserIdentityFlow(userId: String): Flow> { return channelFlow { val userIdentityCollector = UserIdentityCollector(userId, this) val onClose = safeInvokeOnClose { @@ -789,7 +795,8 @@ internal class OlmMachine @Inject constructor( runBlocking { inner.discardRoomKey(roomId) } } - /** Get all the verification requests we have with the given user + /** + * Get all the verification requests we have with the given user. * * @param userId The ID of the user for which we would like to fetch the * verification requests @@ -800,7 +807,7 @@ internal class OlmMachine @Inject constructor( return verificationsProvider.getVerificationRequests(userId) } - /** Get a verification request for the given user with the given flow ID */ + /** Get a verification request for the given user with the given flow ID. */ fun getVerificationRequest(userId: String, flowId: String): VerificationRequest? { return verificationsProvider.getVerificationRequest(userId, flowId) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/PrepareToEncryptUseCase.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/PrepareToEncryptUseCase.kt index 891e1fe3c0..8765d99bca 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/PrepareToEncryptUseCase.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/PrepareToEncryptUseCase.kt @@ -24,6 +24,7 @@ import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.withContext import org.matrix.android.sdk.api.MatrixCoroutineDispatchers import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_MEGOLM +import org.matrix.android.sdk.api.failure.Failure import org.matrix.android.sdk.api.logger.LoggerTag import org.matrix.android.sdk.api.session.crypto.MXCryptoError import org.matrix.android.sdk.internal.crypto.keysbackup.RustKeyBackupService @@ -79,7 +80,7 @@ internal class PrepareToEncryptUseCase @Inject constructor( if (algorithm == null) { val reason = String.format(MXCryptoError.UNABLE_TO_ENCRYPT_REASON, MXCryptoError.NO_MORE_ALGORITHM_REASON) Timber.tag(loggerTag.value).e("prepareToEncrypt() : $reason") - throw IllegalArgumentException("Missing algorithm") + throw Failure.CryptoError(MXCryptoError.Base(MXCryptoError.ErrorType.UNABLE_TO_ENCRYPT, reason)) } preshareRoomKey(roomId, userIds, forceDistributeToUnverified) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/RustCrossSigningService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/RustCrossSigningService.kt index 5a200a59ff..c4fd06bcd0 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/RustCrossSigningService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/RustCrossSigningService.kt @@ -39,7 +39,7 @@ internal class RustCrossSigningService @Inject constructor( ) : CrossSigningService { /** - * Is our own identity trusted + * Is our own identity trusted. */ override suspend fun isCrossSigningVerified(): Boolean { return when (val identity = olmMachine.getIdentity(olmMachine.userId())) { @@ -104,7 +104,7 @@ internal class RustCrossSigningService @Inject constructor( } /** - * Get the public cross signing keys for the given user + * Get the public cross signing keys for the given user. * * @param otherUserId The ID of the user for which we would like to fetch the cross signing keys. */ @@ -131,7 +131,7 @@ internal class RustCrossSigningService @Inject constructor( } /** - * Can we sign our other devices or other users? + * Can we sign our other devices or other users. * * Returning true means that we have the private self-signing and user-signing keys at hand. */ @@ -165,7 +165,7 @@ internal class RustCrossSigningService @Inject constructor( } /** - * Sign one of your devices and upload the signature + * Sign one of your devices and upload the signature. */ override suspend fun trustDevice(deviceId: String) { val device = olmMachine.getDevice(olmMachine.userId(), deviceId) @@ -174,15 +174,15 @@ internal class RustCrossSigningService @Inject constructor( if (verified) { return } else { - throw IllegalArgumentException("This device [$deviceId] is not known, or not yours") + require(false) { "This device [$deviceId] is not known, or not yours" } } } else { - throw IllegalArgumentException("This device [$deviceId] is not known") + require(false) { "This device [$deviceId] is not known" } } } /** - * Check if a device is trusted + * Check if a device is trusted. * * This will check that we have a valid trust chain from our own master key to a device, either * using the self-signing key for our own devices or using the user-signing key and the master diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/RustKeyBackupService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/RustKeyBackupService.kt index c3d172835a..cffee25bc0 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/RustKeyBackupService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/RustKeyBackupService.kt @@ -855,7 +855,7 @@ internal class RustKeyBackupService @Inject constructor( } /** - * Do a backup if there are new keys, with a delay + * Do a backup if there are new keys, with a delay. */ suspend fun maybeBackupKeys() { withContext(coroutineDispatchers.crypto) { @@ -886,7 +886,7 @@ internal class RustKeyBackupService @Inject constructor( } /** - * Send a chunk of keys to backup + * Send a chunk of keys to backup. */ private suspend fun backupKeys(forceRecheck: Boolean = false) { Timber.v("backupKeys") diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/RustCryptoStore.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/RustCryptoStore.kt index b242a3ed34..bf9a5ebf13 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/RustCryptoStore.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/RustCryptoStore.kt @@ -70,7 +70,7 @@ private val loggerTag = LoggerTag("RealmCryptoStore", LoggerTag.CRYPTO) /** * In the transition phase, the rust SDK is still using parts to the realm crypto store, - * this should be removed after full migration + * this should be removed after full migration. */ @SessionScope internal class RustCryptoStore @Inject constructor( @@ -118,6 +118,7 @@ internal class RustCryptoStore @Inject constructor( /** * Retrieve a device by its identity key. * + * @param userId The device owner userId. * @param identityKey the device identity key (`MXDeviceInfo.identityKey`) * @return the device or null if not found */ @@ -134,7 +135,7 @@ internal class RustCryptoStore @Inject constructor( } /** - * Needed for lazy migration of sessions from the legacy store + * Needed for lazy migration of sessions from the legacy store. */ override fun getInboundGroupSession(sessionId: String, senderKey: String): MXInboundMegolmSessionWrapper? { val key = OlmInboundGroupSessionEntity.createPrimaryKey(sessionId, senderKey) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/rust/ExtractUtils.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/rust/RealmToMigrate.kt similarity index 100% rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/rust/ExtractUtils.kt rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/rust/RealmToMigrate.kt diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/RustVerificationService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/RustVerificationService.kt index 35965d6f2e..5a66f93b7c 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/RustVerificationService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/RustVerificationService.kt @@ -40,14 +40,16 @@ import org.matrix.rustcomponents.sdk.crypto.VerificationRequestState import timber.log.Timber import javax.inject.Inject -/** A helper class to deserialize to-device `m.key.verification.*` events to fetch the transaction id out */ +/** + * A helper class to deserialize to-device `m.key.verification.*` events to fetch the transaction id out. + */ @JsonClass(generateAdapter = true) internal data class ToDeviceVerificationEvent( @Json(name = "sender") val sender: String?, @Json(name = "transaction_id") val transactionId: String ) -/** Helper method to fetch the unique ID of the verification event */ +/** Helper method to fetch the unique ID of the verification event. */ private fun getFlowId(event: Event): String? { return if (event.eventId != null) { event.getRelationContent()?.eventId @@ -57,7 +59,7 @@ private fun getFlowId(event: Event): String? { } } -/** Convert a list of VerificationMethod into a list of strings that can be passed to the Rust side */ +/** Convert a list of VerificationMethod into a list of strings that can be passed to the Rust side. */ internal fun prepareMethods(methods: List): List { val stringMethods: MutableList = methods.map { it.toValue() }.toMutableList() @@ -147,7 +149,7 @@ internal class RustVerificationService @Inject constructor( } } - /** Check if the start event created new verification objects and dispatch updates */ + /** Check if the start event created new verification objects and dispatch updates. */ private suspend fun onStart(event: Event) { if (event.unsignedData?.transactionId != null) return // remote echo val sender = event.senderId ?: return @@ -186,7 +188,7 @@ internal class RustVerificationService @Inject constructor( } } - /** Check if the request event created a nev verification request object and dispatch that it dis so */ + /** Check if the request event created a nev verification request object and dispatch that it dis so. */ private suspend fun onRequest(event: Event, fromRoomMessage: Boolean) { val flowId = if (fromRoomMessage) { event.eventId diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/SasVerification.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/SasVerification.kt index 12ca5ae6e5..13afdd7b5b 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/SasVerification.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/SasVerification.kt @@ -35,7 +35,7 @@ import org.matrix.rustcomponents.sdk.crypto.Sas import org.matrix.rustcomponents.sdk.crypto.SasListener import org.matrix.rustcomponents.sdk.crypto.SasState -/** Class representing a short auth string verification flow */ +/** Class representing a short auth string verification flow. */ internal class SasVerification @AssistedInject constructor( @Assisted private var inner: Sas, // private val olmMachine: OlmMachine, @@ -56,14 +56,14 @@ internal class SasVerification @AssistedInject constructor( fun create(inner: Sas): SasVerification } - /** The user ID of the other user that is participating in this verification flow */ + /** The user ID of the other user that is participating in this verification flow. */ override val otherUserId: String = inner.otherUserId() - /** Get the device id of the other user's device participating in this verification flow */ + /** Get the device id of the other user's device participating in this verification flow. */ override val otherDeviceId: String get() = inner.otherDeviceId() - /** Did the other side initiate this verification flow */ + /** Did the other side initiate this verification flow. */ override val isIncoming: Boolean get() = !inner.weStarted() @@ -85,11 +85,11 @@ internal class SasVerification @AssistedInject constructor( } } - /** Get the unique id of this verification */ + /** Get the unique id of this verification. */ override val transactionId: String get() = inner.flowId() - /** Cancel the verification flow + /** Cancel the verification flow. * * This will send out a m.key.verification.cancel event with the cancel * code set to m.user. @@ -102,7 +102,7 @@ internal class SasVerification @AssistedInject constructor( cancelHelper(CancelCode.User) } - /** Cancel the verification flow + /** Cancel the verification flow. * * This will send out a m.key.verification.cancel event with the cancel * code set to the given CancelCode. @@ -117,7 +117,7 @@ internal class SasVerification @AssistedInject constructor( cancelHelper(code) } - /** Cancel the verification flow + /** Cancel the verification flow. * * This will send out a m.key.verification.cancel event with the cancel * code set to the m.mismatched_sas cancel code. @@ -141,7 +141,7 @@ internal class SasVerification @AssistedInject constructor( // return inner.supportsEmoji() // } - /** Confirm that the short authentication code matches on both sides + /** Confirm that the short authentication code matches on both sides. * * This sends a m.key.verification.mac event out, the verification isn't yet * done, we still need to receive such an event from the other side if we haven't @@ -154,7 +154,7 @@ internal class SasVerification @AssistedInject constructor( confirm() } - /** Accept the verification flow, signaling the other side that we do want to verify + /** Accept the verification flow, signaling the other side that we do want to verify. * * This sends a m.key.verification.accept event out that is a response to a * m.key.verification.start event from the other side. @@ -166,7 +166,7 @@ internal class SasVerification @AssistedInject constructor( accept() } - /** Get the decimal representation of the short auth string + /** Get the decimal representation of the short auth string. * * @return A string of three space delimited numbers that * represent the short auth string or an empty string if we're not yet @@ -176,7 +176,7 @@ internal class SasVerification @AssistedInject constructor( return decimals?.joinToString(" ") ?: "" } - /** Get the emoji representation of the short auth string + /** Get the emoji representation of the short auth string. * * @return A list of 7 EmojiRepresentation objects that represent the * short auth string or an empty list if we're not yet in a presentable diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/QrCodeVerification.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/QrCodeVerification.kt index dcf4c4013d..03df266108 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/QrCodeVerification.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/QrCodeVerification.kt @@ -36,7 +36,7 @@ import org.matrix.rustcomponents.sdk.crypto.QrCode import org.matrix.rustcomponents.sdk.crypto.QrCodeState import timber.log.Timber -/** Class representing a QR code based verification flow */ +/** Class representing a QR code based verification flow. */ internal class QrCodeVerification @AssistedInject constructor( @Assisted private var inner: QrCode, private val olmMachine: OlmMachine, @@ -86,12 +86,12 @@ internal class QrCodeVerification @AssistedInject constructor( // dispatchTxUpdated() // } - /** Confirm that the other side has indeed scanned the QR code we presented */ + /** Confirm that the other side has indeed scanned the QR code we presented. */ override suspend fun otherUserScannedMyQrCode() { confirm() } - /** Cancel the QR code verification, denying that the other side has scanned the QR code */ + /** Cancel the QR code verification, denying that the other side has scanned the QR code. */ override suspend fun otherUserDidNotScannedMyQrCode() { // TODO Is this code correct here? The old code seems to do this cancelHelper(CancelCode.MismatchedKeys) @@ -115,26 +115,26 @@ internal class QrCodeVerification @AssistedInject constructor( } } - /** Get the unique id of this verification */ + /** Get the unique id of this verification. */ override val transactionId: String get() = inner.flowId() - /** Get the user id of the other user participating in this verification flow */ + /** Get the user id of the other user participating in this verification flow. */ override val otherUserId: String get() = inner.otherUserId() - /** Get the device id of the other user's device participating in this verification flow */ + /** Get the device id of the other user's device participating in this verification flow. */ override var otherDeviceId: String? get() = inner.otherDeviceId() @Suppress("UNUSED_PARAMETER") set(value) { } - /** Did the other side initiate this verification flow */ + /** Did the other side initiate this verification flow. */ override val isIncoming: Boolean get() = !inner.weStarted() - /** Cancel the verification flow + /** Cancel the verification flow. * * This will send out a m.key.verification.cancel event with the cancel * code set to m.user. @@ -147,7 +147,7 @@ internal class QrCodeVerification @AssistedInject constructor( cancelHelper(CancelCode.User) } - /** Cancel the verification flow + /** Cancel the verification flow. * * This will send out a m.key.verification.cancel event with the cancel * code set to the given CancelCode. @@ -162,12 +162,12 @@ internal class QrCodeVerification @AssistedInject constructor( cancelHelper(code) } - /** Is this verification happening over to-device messages */ + /** Is this verification happening over to-device messages. */ override fun isToDeviceTransport(): Boolean { return inner.roomId() == null } - /** Confirm the QR code verification + /** Confirm the QR code verification. * * This confirms that the other side has scanned our QR code and sends * out a m.key.verification.done event to the other side. @@ -202,7 +202,7 @@ internal class QrCodeVerification @AssistedInject constructor( } } - /** Fetch fresh data from the Rust side for our verification flow */ + /** Fetch fresh data from the Rust side for our verification flow. */ private fun refreshData() { innerMachine.getVerification(inner.otherUserId(), inner.flowId()) ?.asQr()?.let { From a6b127cb20a24665d0dc5abc44c4bc55bde7dbaf Mon Sep 17 00:00:00 2001 From: Valere Date: Mon, 2 Oct 2023 15:31:11 +0200 Subject: [PATCH 04/61] code quality --- .../crypto/keysbackup/BackupRecoveryKey.kt | 2 +- .../sdk/internal/crypto/RustCryptoService.kt | 40 +++++-------------- 2 files changed, 11 insertions(+), 31 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/keysbackup/BackupRecoveryKey.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/keysbackup/BackupRecoveryKey.kt index 48db9c5ebf..b3b6ef1fc7 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/keysbackup/BackupRecoveryKey.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/keysbackup/BackupRecoveryKey.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 New Vector Ltd + * Copyright 2023 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. diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/RustCryptoService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/RustCryptoService.kt index d5069fe010..b3a3c35389 100755 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/RustCryptoService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/RustCryptoService.kt @@ -337,7 +337,7 @@ internal class RustCryptoService @Inject constructor( /** * Provides the device information for a user id and a device Id * - * @param userId the user id + * @param userId the user id * @param deviceId the device id */ override suspend fun getCryptoDeviceInfo(userId: String, deviceId: String?): CryptoDeviceInfo? { @@ -386,9 +386,9 @@ internal class RustCryptoService @Inject constructor( /** * Configure a room to use encryption. * - * @param roomId the room id to enable encryption in. - * @param algorithm the encryption config for the room. - * @param membersId list of members to start tracking their devices + * @param roomId the room id to enable encryption in. + * @param algorithm the encryption config for the room. + * @param membersId list of members to start tracking their devices * @return true if the operation succeeds. */ private suspend fun setEncryptionInRoom( @@ -470,8 +470,8 @@ internal class RustCryptoService @Inject constructor( * Encrypt an event content according to the configuration of the room. * * @param eventContent the content of the event. - * @param eventType the type of the event. - * @param roomId the room identifier the event will be sent. + * @param eventType the type of the event. + * @param roomId the room identifier the event will be sent. */ override suspend fun encryptEventContent( eventContent: Content, @@ -488,7 +488,7 @@ internal class RustCryptoService @Inject constructor( /** * Decrypt an event * - * @param event the raw event. + * @param event the raw event. * @param timeline the id of the timeline where the event is decrypted. It is used to prevent replay attack. * @return the MXEventDecryptionResult data, or throw in case of error */ @@ -681,8 +681,8 @@ internal class RustCryptoService @Inject constructor( /** * Import the room keys * - * @param roomKeysAsArray the room keys as array. - * @param password the password + * @param roomKeysAsArray the room keys as array. + * @param password the password * @param progressListener the progress listener * @return the result ImportRoomKeysResult */ @@ -715,7 +715,7 @@ internal class RustCryptoService @Inject constructor( * If false, it can still be overridden per-room. * If true, it overrides the per-room settings. * - * @param block true to unilaterally blacklist all + * @param block true to unilaterally blacklist all */ override fun setGlobalBlacklistUnverifiedDevices(block: Boolean) { cryptoStore.setGlobalBlacklistUnverifiedDevices(block) @@ -785,26 +785,6 @@ internal class RustCryptoService @Inject constructor( return cryptoStore.getLiveBlockUnverifiedDevices(roomId) } -// /** -// * Manages the room black-listing for unverified devices. -// * -// * @param roomId the room id -// * @param add true to add the room id to the list, false to remove it. -// */ -// private fun setRoomBlacklistUnverifiedDevices(roomId: String, add: Boolean) { -// val roomIds = cryptoStore.getRoomsListBlacklistUnverifiedDevices().toMutableList() -// -// if (add) { -// if (roomId !in roomIds) { -// roomIds.add(roomId) -// } -// } else { -// roomIds.remove(roomId) -// } -// -// cryptoStore.setRoomsListBlacklistUnverifiedDevices(roomIds) -// } - /** * Re request the encryption keys required to decrypt an event. * From ecd1057ce990f228644e8db0cc98220ae19a2709 Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 5 Oct 2023 17:00:51 +0200 Subject: [PATCH 05/61] version++ --- matrix-sdk-android/build.gradle | 2 +- vector-app/build.gradle | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/matrix-sdk-android/build.gradle b/matrix-sdk-android/build.gradle index 63fb13ead8..00d91d3835 100644 --- a/matrix-sdk-android/build.gradle +++ b/matrix-sdk-android/build.gradle @@ -63,7 +63,7 @@ android { // that the app's state is completely cleared between tests. testInstrumentationRunnerArguments clearPackageData: 'true' - buildConfigField "String", "SDK_VERSION", "\"1.6.6\"" + buildConfigField "String", "SDK_VERSION", "\"1.6.8\"" buildConfigField "String", "GIT_SDK_REVISION", "\"${gitRevision()}\"" buildConfigField "String", "GIT_SDK_REVISION_UNIX_DATE", "\"${gitRevisionUnixDate()}\"" diff --git a/vector-app/build.gradle b/vector-app/build.gradle index 9aff3eaa84..ce05b92902 100644 --- a/vector-app/build.gradle +++ b/vector-app/build.gradle @@ -37,7 +37,7 @@ ext.versionMinor = 6 // Note: even values are reserved for regular release, odd values for hotfix release. // When creating a hotfix, you should decrease the value, since the current value // is the value for the next regular release. -ext.versionPatch = 6 +ext.versionPatch = 8 static def getGitTimestamp() { def cmd = 'git show -s --format=%ct' From 52082a9defdb9b836a7261d6ece74217709764c9 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 6 Oct 2023 10:41:15 +0200 Subject: [PATCH 06/61] Ensure the incoming call will not ring forever, in case the call is not ended by another way (#8178) Add a safe 2 minutes timer. --- changelog.d/8178.bugfix | 1 + .../app/features/call/webrtc/WebRtcCallManager.kt | 10 ++++++++++ 2 files changed, 11 insertions(+) create mode 100644 changelog.d/8178.bugfix diff --git a/changelog.d/8178.bugfix b/changelog.d/8178.bugfix new file mode 100644 index 0000000000..e7f073f4fc --- /dev/null +++ b/changelog.d/8178.bugfix @@ -0,0 +1 @@ +Ensure the incoming call will not ring forever, in case the call is not ended by another way. diff --git a/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCallManager.kt b/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCallManager.kt index 074779c6cc..1eb4134a87 100644 --- a/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCallManager.kt +++ b/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCallManager.kt @@ -34,6 +34,8 @@ import im.vector.app.features.call.vectorCallService import im.vector.app.features.session.coroutineScope import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.asCoroutineDispatcher +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.logger.LoggerTag @@ -386,6 +388,14 @@ class WebRtcCallManager @Inject constructor( // Maybe increase sync freq? but how to set back to default values? } } + + // ensure the incoming call will not ring forever + sessionScope?.launch { + delay(2 * 60 * 1000 /* 2 minutes */) + if (mxCall.state is CallState.LocalRinging) { + onCallEnded(mxCall.callId, EndCallReason.INVITE_TIMEOUT, rejected = false) + } + } } override fun onCallAnswerReceived(callAnswerContent: CallAnswerContent) { From 842aeb70e0ae3837cfe0abd5b400696a46021c09 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 6 Oct 2023 18:03:45 +0200 Subject: [PATCH 07/61] Parse "io.element.disable_network_constraint" from .well-known file. Migrate DB to 54. --- .../android/sdk/api/auth/data/WellKnown.kt | 6 ++++ .../homeserver/HomeServerCapabilities.kt | 5 ++++ .../database/RealmSessionStoreMigration.kt | 4 ++- .../mapper/HomeServerCapabilitiesMapper.kt | 1 + .../database/migration/MigrateSessionTo054.kt | 30 +++++++++++++++++++ .../model/HomeServerCapabilitiesEntity.kt | 1 + .../GetHomeServerCapabilitiesTask.kt | 2 ++ 7 files changed, 48 insertions(+), 1 deletion(-) create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo054.kt diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/WellKnown.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/WellKnown.kt index 95488bd682..2f5863d1f4 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/WellKnown.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/WellKnown.kt @@ -61,4 +61,10 @@ data class WellKnown( */ @Json(name = "org.matrix.msc2965.authentication") val unstableDelegatedAuthConfig: DelegatedAuthConfig? = null, + + /** + * If set to true, the SDK will not use the network constraint when configuring Worker for the WorkManager. + */ + @Json(name = "io.element.disable_network_constraint") + val disableNetworkConstraint: Boolean? = null, ) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/homeserver/HomeServerCapabilities.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/homeserver/HomeServerCapabilities.kt index 6b94452e39..a2ea5c4133 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/homeserver/HomeServerCapabilities.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/homeserver/HomeServerCapabilities.kt @@ -90,6 +90,11 @@ data class HomeServerCapabilities( * Authentication issuer for use with MSC3824 delegated OIDC, provided in Wellknown. */ val authenticationIssuer: String? = null, + + /** + * If set to true, the SDK will not use the network constraint when configuring Worker for the WorkManager, provided in Wellknown. + */ + val disableNetworkConstraint: Boolean = false, ) { enum class RoomCapabilitySupport { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt index 4376323df4..4a7064ebf5 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt @@ -70,6 +70,7 @@ import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo050 import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo051 import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo052 import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo053 +import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo054 import org.matrix.android.sdk.internal.util.Normalizer import org.matrix.android.sdk.internal.util.database.MatrixRealmMigration import javax.inject.Inject @@ -78,7 +79,7 @@ internal class RealmSessionStoreMigration @Inject constructor( private val normalizer: Normalizer ) : MatrixRealmMigration( dbName = "Session", - schemaVersion = 53L, + schemaVersion = 54L, ) { /** * Forces all RealmSessionStoreMigration instances to be equal. @@ -141,5 +142,6 @@ internal class RealmSessionStoreMigration @Inject constructor( if (oldVersion < 51) MigrateSessionTo051(realm).perform() if (oldVersion < 52) MigrateSessionTo052(realm).perform() if (oldVersion < 53) MigrateSessionTo053(realm).perform() + if (oldVersion < 54) MigrateSessionTo054(realm).perform() } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/HomeServerCapabilitiesMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/HomeServerCapabilitiesMapper.kt index f5cf88e2c1..25af5be66d 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/HomeServerCapabilitiesMapper.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/HomeServerCapabilitiesMapper.kt @@ -50,6 +50,7 @@ internal object HomeServerCapabilitiesMapper { canRedactRelatedEvents = entity.canRedactEventWithRelations, externalAccountManagementUrl = entity.externalAccountManagementUrl, authenticationIssuer = entity.authenticationIssuer, + disableNetworkConstraint = entity.disableNetworkConstraint, ) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo054.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo054.kt new file mode 100644 index 0000000000..0b58c8093d --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo054.kt @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2023 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. + */ + +package org.matrix.android.sdk.internal.database.migration + +import io.realm.DynamicRealm +import org.matrix.android.sdk.internal.database.model.HomeServerCapabilitiesEntityFields +import org.matrix.android.sdk.internal.extensions.forceRefreshOfHomeServerCapabilities +import org.matrix.android.sdk.internal.util.database.RealmMigrator + +internal class MigrateSessionTo054(realm: DynamicRealm) : RealmMigrator(realm, 54) { + override fun doMigrate(realm: DynamicRealm) { + realm.schema.get("HomeServerCapabilitiesEntity") + ?.addField(HomeServerCapabilitiesEntityFields.DISABLE_NETWORK_CONSTRAINT, Boolean::class.java) + ?.forceRefreshOfHomeServerCapabilities() + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/HomeServerCapabilitiesEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/HomeServerCapabilitiesEntity.kt index 79aaf6d4ce..a00dc29087 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/HomeServerCapabilitiesEntity.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/HomeServerCapabilitiesEntity.kt @@ -37,6 +37,7 @@ internal open class HomeServerCapabilitiesEntity( var canRedactEventWithRelations: Boolean = false, var externalAccountManagementUrl: String? = null, var authenticationIssuer: String? = null, + var disableNetworkConstraint: Boolean = false, ) : RealmObject() { companion object diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetHomeServerCapabilitiesTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetHomeServerCapabilitiesTask.kt index b973de9fd3..05997aeadd 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetHomeServerCapabilitiesTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetHomeServerCapabilitiesTask.kt @@ -20,6 +20,7 @@ import com.zhuinden.monarchy.Monarchy import org.matrix.android.sdk.api.MatrixPatterns.getServerName import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig import org.matrix.android.sdk.api.auth.wellknown.WellknownResult +import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.extensions.orTrue import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilities import org.matrix.android.sdk.internal.auth.version.Versions @@ -167,6 +168,7 @@ internal class DefaultGetHomeServerCapabilitiesTask @Inject constructor( } homeServerCapabilitiesEntity.authenticationIssuer = getWellknownResult.wellKnown.unstableDelegatedAuthConfig?.issuer homeServerCapabilitiesEntity.externalAccountManagementUrl = getWellknownResult.wellKnown.unstableDelegatedAuthConfig?.accountManagementUrl + homeServerCapabilitiesEntity.disableNetworkConstraint = getWellknownResult.wellKnown.disableNetworkConstraint.orFalse() } homeServerCapabilitiesEntity.canLoginWithQrCode = canLoginWithQrCode(getCapabilitiesResult, getVersionResult) From 2a5e233e2c30b1132988754ea758a58cf18b6c00 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 9 Oct 2023 11:44:40 +0200 Subject: [PATCH 08/61] Do not apply network constraint if "io.element.disable_network_constraint" is set to true in .well-known file. --- .../sdk/internal/di/WorkManagerProvider.kt | 16 ++++++++++------ .../session/pushers/DefaultPushersService.kt | 6 ++++-- .../session/room/send/DefaultSendService.kt | 6 ++++-- .../room/timeline/TimelineSendEventWorkCommon.kt | 6 ++++-- .../internal/session/sync/DefaultSyncService.kt | 16 ++++++++++++++-- .../sdk/internal/session/sync/job/SyncWorker.kt | 10 ++++++++-- 6 files changed, 44 insertions(+), 16 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/WorkManagerProvider.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/WorkManagerProvider.kt index d8cdd162f1..e08f120e38 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/WorkManagerProvider.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/WorkManagerProvider.kt @@ -29,7 +29,9 @@ import androidx.work.WorkRequest import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import org.matrix.android.sdk.api.MatrixCoroutineDispatchers +import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.internal.session.SessionScope +import org.matrix.android.sdk.internal.session.homeserver.HomeServerCapabilitiesDataSource import org.matrix.android.sdk.internal.worker.MatrixWorkerFactory import java.util.concurrent.TimeUnit import javax.inject.Inject @@ -102,12 +104,14 @@ internal class WorkManagerProvider @Inject constructor( companion object { private const val MATRIX_SDK_TAG_PREFIX = "MatrixSDK-" - /** - * Default constraints: connected network. - */ - val workConstraints = Constraints.Builder() - .setRequiredNetworkType(NetworkType.CONNECTED) - .build() + fun getWorkConstraints( + homeServerCapabilitiesDataSource: HomeServerCapabilitiesDataSource, + ): Constraints { + val withNetworkConstraint = homeServerCapabilitiesDataSource.getHomeServerCapabilities()?.disableNetworkConstraint.orFalse().not() + return Constraints.Builder() + .apply { if (withNetworkConstraint) setRequiredNetworkType(NetworkType.CONNECTED) } + .build() + } // Use min value, smaller value will be ignored const val BACKOFF_DELAY_MILLIS = WorkRequest.MIN_BACKOFF_MILLIS diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/DefaultPushersService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/DefaultPushersService.kt index e89cfa508c..0d73d79662 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/DefaultPushersService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/DefaultPushersService.kt @@ -27,6 +27,7 @@ import org.matrix.android.sdk.internal.database.query.where import org.matrix.android.sdk.internal.di.SessionDatabase import org.matrix.android.sdk.internal.di.SessionId import org.matrix.android.sdk.internal.di.WorkManagerProvider +import org.matrix.android.sdk.internal.session.homeserver.HomeServerCapabilitiesDataSource import org.matrix.android.sdk.internal.session.pushers.gateway.PushGatewayNotifyTask import org.matrix.android.sdk.internal.task.TaskExecutor import org.matrix.android.sdk.internal.task.configureWith @@ -44,7 +45,8 @@ internal class DefaultPushersService @Inject constructor( private val addPusherTask: AddPusherTask, private val togglePusherTask: TogglePusherTask, private val removePusherTask: RemovePusherTask, - private val taskExecutor: TaskExecutor + private val taskExecutor: TaskExecutor, + private val homeServerCapabilitiesDataSource: HomeServerCapabilitiesDataSource, ) : PushersService { override suspend fun testPush( @@ -130,7 +132,7 @@ internal class DefaultPushersService @Inject constructor( private fun enqueueAddPusher(pusher: JsonPusher): UUID { val params = AddPusherWorker.Params(sessionId, pusher) val request = workManagerProvider.matrixOneTimeWorkRequestBuilder() - .setConstraints(WorkManagerProvider.workConstraints) + .setConstraints(WorkManagerProvider.getWorkConstraints(homeServerCapabilitiesDataSource)) .setInputData(WorkerParamsFactory.toData(params)) .setBackoffCriteria(BackoffPolicy.LINEAR, WorkManagerProvider.BACKOFF_DELAY_MILLIS, TimeUnit.MILLISECONDS) .build() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt index 04749103c1..dd2d79741a 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt @@ -53,6 +53,7 @@ import org.matrix.android.sdk.internal.crypto.store.IMXCommonCryptoStore import org.matrix.android.sdk.internal.di.SessionId import org.matrix.android.sdk.internal.di.WorkManagerProvider import org.matrix.android.sdk.internal.session.content.UploadContentWorker +import org.matrix.android.sdk.internal.session.homeserver.HomeServerCapabilitiesDataSource import org.matrix.android.sdk.internal.session.room.send.queue.EventSenderProcessor import org.matrix.android.sdk.internal.task.TaskExecutor import org.matrix.android.sdk.internal.util.CancelableWork @@ -73,7 +74,8 @@ internal class DefaultSendService @AssistedInject constructor( private val taskExecutor: TaskExecutor, private val localEchoRepository: LocalEchoRepository, private val eventSenderProcessor: EventSenderProcessor, - private val cancelSendTracker: CancelSendTracker + private val cancelSendTracker: CancelSendTracker, + private val homeServerCapabilitiesDataSource: HomeServerCapabilitiesDataSource, ) : SendService { @AssistedFactory @@ -373,7 +375,7 @@ internal class DefaultSendService @AssistedInject constructor( val uploadWorkData = WorkerParamsFactory.toData(uploadMediaWorkerParams) return workManagerProvider.matrixOneTimeWorkRequestBuilder() - .setConstraints(WorkManagerProvider.workConstraints) + .setConstraints(WorkManagerProvider.getWorkConstraints(homeServerCapabilitiesDataSource)) .startChain(true) .setInputData(uploadWorkData) .setBackoffCriteria(BackoffPolicy.LINEAR, WorkManagerProvider.BACKOFF_DELAY_MILLIS, TimeUnit.MILLISECONDS) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineSendEventWorkCommon.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineSendEventWorkCommon.kt index 21b508d35a..0295a857fe 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineSendEventWorkCommon.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineSendEventWorkCommon.kt @@ -22,6 +22,7 @@ import androidx.work.ListenableWorker import androidx.work.OneTimeWorkRequest import org.matrix.android.sdk.api.util.Cancelable import org.matrix.android.sdk.internal.di.WorkManagerProvider +import org.matrix.android.sdk.internal.session.homeserver.HomeServerCapabilitiesDataSource import org.matrix.android.sdk.internal.util.CancelableWork import org.matrix.android.sdk.internal.worker.startChain import java.util.concurrent.TimeUnit @@ -34,7 +35,8 @@ import javax.inject.Inject * if not the chain will be doomed in failed state. */ internal class TimelineSendEventWorkCommon @Inject constructor( - private val workManagerProvider: WorkManagerProvider + private val workManagerProvider: WorkManagerProvider, + private val homeServerCapabilitiesDataSource: HomeServerCapabilitiesDataSource, ) { fun postWork(roomId: String, workRequest: OneTimeWorkRequest, policy: ExistingWorkPolicy = ExistingWorkPolicy.APPEND_OR_REPLACE): Cancelable { @@ -47,7 +49,7 @@ internal class TimelineSendEventWorkCommon @Inject constructor( inline fun createWork(data: Data, startChain: Boolean): OneTimeWorkRequest { return workManagerProvider.matrixOneTimeWorkRequestBuilder() - .setConstraints(WorkManagerProvider.workConstraints) + .setConstraints(WorkManagerProvider.getWorkConstraints(homeServerCapabilitiesDataSource)) .startChain(startChain) .setInputData(data) .setBackoffCriteria(BackoffPolicy.LINEAR, WorkManagerProvider.BACKOFF_DELAY_MILLIS, TimeUnit.MILLISECONDS) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/DefaultSyncService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/DefaultSyncService.kt index 76c3c38abf..5c3ca5a6b1 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/DefaultSyncService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/DefaultSyncService.kt @@ -20,6 +20,7 @@ import org.matrix.android.sdk.api.session.sync.SyncService import org.matrix.android.sdk.internal.di.SessionId import org.matrix.android.sdk.internal.di.WorkManagerProvider import org.matrix.android.sdk.internal.session.SessionState +import org.matrix.android.sdk.internal.session.homeserver.HomeServerCapabilitiesDataSource import org.matrix.android.sdk.internal.session.sync.job.SyncThread import org.matrix.android.sdk.internal.session.sync.job.SyncWorker import timber.log.Timber @@ -33,15 +34,26 @@ internal class DefaultSyncService @Inject constructor( private val syncTokenStore: SyncTokenStore, private val syncRequestStateTracker: SyncRequestStateTracker, private val sessionState: SessionState, + private val homeServerCapabilitiesDataSource: HomeServerCapabilitiesDataSource, ) : SyncService { private var syncThread: SyncThread? = null override fun requireBackgroundSync() { - SyncWorker.requireBackgroundSync(workManagerProvider, sessionId) + SyncWorker.requireBackgroundSync( + workManagerProvider = workManagerProvider, + sessionId = sessionId, + homeServerCapabilitiesDataSource = homeServerCapabilitiesDataSource, + ) } override fun startAutomaticBackgroundSync(timeOutInSeconds: Long, repeatDelayInSeconds: Long) { - SyncWorker.automaticallyBackgroundSync(workManagerProvider, sessionId, timeOutInSeconds, repeatDelayInSeconds) + SyncWorker.automaticallyBackgroundSync( + workManagerProvider = workManagerProvider, + sessionId = sessionId, + homeServerCapabilitiesDataSource = homeServerCapabilitiesDataSource, + serverTimeoutInSeconds = timeOutInSeconds, + delayInSeconds = repeatDelayInSeconds, + ) } override fun stopAnyBackgroundSync() { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/job/SyncWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/job/SyncWorker.kt index a04bc74628..15002ed43a 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/job/SyncWorker.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/job/SyncWorker.kt @@ -25,6 +25,7 @@ import org.matrix.android.sdk.api.failure.isTokenError import org.matrix.android.sdk.internal.SessionManager import org.matrix.android.sdk.internal.di.WorkManagerProvider import org.matrix.android.sdk.internal.session.SessionComponent +import org.matrix.android.sdk.internal.session.homeserver.HomeServerCapabilitiesDataSource import org.matrix.android.sdk.internal.session.sync.SyncPresence import org.matrix.android.sdk.internal.session.sync.SyncTask import org.matrix.android.sdk.internal.worker.SessionSafeCoroutineWorker @@ -59,6 +60,7 @@ internal class SyncWorker(context: Context, workerParameters: WorkerParameters, @Inject lateinit var syncTask: SyncTask @Inject lateinit var workManagerProvider: WorkManagerProvider + @Inject lateinit var homeServerCapabilitiesDataSource: HomeServerCapabilitiesDataSource override fun injectWith(injector: SessionComponent) { injector.inject(this) @@ -77,6 +79,7 @@ internal class SyncWorker(context: Context, workerParameters: WorkerParameters, automaticallyBackgroundSync( workManagerProvider = workManagerProvider, sessionId = params.sessionId, + homeServerCapabilitiesDataSource = homeServerCapabilitiesDataSource, serverTimeoutInSeconds = params.timeout, delayInSeconds = params.delay, forceImmediate = hasToDeviceEvents @@ -86,6 +89,7 @@ internal class SyncWorker(context: Context, workerParameters: WorkerParameters, requireBackgroundSync( workManagerProvider = workManagerProvider, sessionId = params.sessionId, + homeServerCapabilitiesDataSource = homeServerCapabilitiesDataSource, serverTimeoutInSeconds = 0 ) } @@ -123,6 +127,7 @@ internal class SyncWorker(context: Context, workerParameters: WorkerParameters, fun requireBackgroundSync( workManagerProvider: WorkManagerProvider, sessionId: String, + homeServerCapabilitiesDataSource: HomeServerCapabilitiesDataSource, serverTimeoutInSeconds: Long = 0 ) { val data = WorkerParamsFactory.toData( @@ -134,7 +139,7 @@ internal class SyncWorker(context: Context, workerParameters: WorkerParameters, ) ) val workRequest = workManagerProvider.matrixOneTimeWorkRequestBuilder() - .setConstraints(WorkManagerProvider.workConstraints) + .setConstraints(WorkManagerProvider.getWorkConstraints(homeServerCapabilitiesDataSource)) .setBackoffCriteria(BackoffPolicy.LINEAR, WorkManagerProvider.BACKOFF_DELAY_MILLIS, TimeUnit.MILLISECONDS) .setInputData(data) .startChain(true) @@ -146,6 +151,7 @@ internal class SyncWorker(context: Context, workerParameters: WorkerParameters, fun automaticallyBackgroundSync( workManagerProvider: WorkManagerProvider, sessionId: String, + homeServerCapabilitiesDataSource: HomeServerCapabilitiesDataSource, serverTimeoutInSeconds: Long = 0, delayInSeconds: Long = 30, forceImmediate: Boolean = false @@ -160,7 +166,7 @@ internal class SyncWorker(context: Context, workerParameters: WorkerParameters, ) ) val workRequest = workManagerProvider.matrixOneTimeWorkRequestBuilder() - .setConstraints(WorkManagerProvider.workConstraints) + .setConstraints(WorkManagerProvider.getWorkConstraints(homeServerCapabilitiesDataSource)) .setInputData(data) .setBackoffCriteria(BackoffPolicy.LINEAR, WorkManagerProvider.BACKOFF_DELAY_MILLIS, TimeUnit.MILLISECONDS) .setInitialDelay(if (forceImmediate) 0 else delayInSeconds, TimeUnit.SECONDS) From f13a15495b14d84d124687cfc9f0b4ff83fe9804 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 9 Oct 2023 12:04:35 +0200 Subject: [PATCH 09/61] Add a log when network constraint is disabled. --- .../android/sdk/internal/di/WorkManagerProvider.kt | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/WorkManagerProvider.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/WorkManagerProvider.kt index e08f120e38..1852f05ce0 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/WorkManagerProvider.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/WorkManagerProvider.kt @@ -33,6 +33,7 @@ import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.internal.session.SessionScope import org.matrix.android.sdk.internal.session.homeserver.HomeServerCapabilitiesDataSource import org.matrix.android.sdk.internal.worker.MatrixWorkerFactory +import timber.log.Timber import java.util.concurrent.TimeUnit import javax.inject.Inject @@ -109,7 +110,13 @@ internal class WorkManagerProvider @Inject constructor( ): Constraints { val withNetworkConstraint = homeServerCapabilitiesDataSource.getHomeServerCapabilities()?.disableNetworkConstraint.orFalse().not() return Constraints.Builder() - .apply { if (withNetworkConstraint) setRequiredNetworkType(NetworkType.CONNECTED) } + .apply { + if (withNetworkConstraint) { + setRequiredNetworkType(NetworkType.CONNECTED) + } else { + Timber.w("Network constraint is disabled") + } + } .build() } From 747c81c687c7ff5a94ea8c9c36b0f84680d22136 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 10 Oct 2023 10:48:13 +0200 Subject: [PATCH 10/61] Changelog. --- changelog.d/8662.misc | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/8662.misc diff --git a/changelog.d/8662.misc b/changelog.d/8662.misc new file mode 100644 index 0000000000..4a18120301 --- /dev/null +++ b/changelog.d/8662.misc @@ -0,0 +1 @@ +Take into account boolean "io.element.disable_network_constraint" from the .well-known file. From 973246819ad89f5062db7658f42e7f49e04f488f Mon Sep 17 00:00:00 2001 From: Giom Foret Date: Tue, 10 Oct 2023 14:11:38 +0200 Subject: [PATCH 11/61] Fix Task :matrix-sdk-android:compileKotlinCryptoDebugUnitTestKotlin FAILED e: file:///home/runner/actions-runner/_work/element-android/element-android/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/pushers/DefaultPushersServiceTest.kt:55:5 No value passed for parameter 'homeServerCapabilitiesDataSource' --- .../sdk/internal/session/pushers/DefaultPushersServiceTest.kt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/pushers/DefaultPushersServiceTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/pushers/DefaultPushersServiceTest.kt index a00ac3a17d..47d3dc2085 100644 --- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/pushers/DefaultPushersServiceTest.kt +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/pushers/DefaultPushersServiceTest.kt @@ -21,6 +21,7 @@ import kotlinx.coroutines.test.runTest import org.junit.Test import org.matrix.android.sdk.test.fakes.FakeAddPusherTask import org.matrix.android.sdk.test.fakes.FakeGetPushersTask +import org.matrix.android.sdk.test.fakes.FakeHomeServerCapabilitiesDataSource import org.matrix.android.sdk.test.fakes.FakeMonarchy import org.matrix.android.sdk.test.fakes.FakeRemovePusherTask import org.matrix.android.sdk.test.fakes.FakeTaskExecutor @@ -41,6 +42,7 @@ class DefaultPushersServiceTest { private val togglePusherTask = FakeTogglePusherTask() private val removePusherTask = FakeRemovePusherTask() private val taskExecutor = FakeTaskExecutor() + private val homeServerCapabilitiesDataSource = FakeHomeServerCapabilitiesDataSource() private val pushersService = DefaultPushersService( workManagerProvider.instance, @@ -52,6 +54,7 @@ class DefaultPushersServiceTest { togglePusherTask, removePusherTask, taskExecutor.instance, + homeServerCapabilitiesDataSource.instance, ) @Test From e27916f85e73802f4b5e299581e6c4e4222a6db4 Mon Sep 17 00:00:00 2001 From: Valere Date: Tue, 10 Oct 2023 16:25:03 +0200 Subject: [PATCH 12/61] detekt fix --- .../crypto/PrepareToEncryptUseCase.kt | 4 ++-- .../sdk/internal/crypto/RustCryptoService.kt | 23 +++++++++++-------- .../crypto/keysbackup/RustKeyBackupService.kt | 2 +- .../store/db/migration/MigrateCryptoTo022.kt | 2 +- .../verification/RustVerificationService.kt | 6 ++--- .../crypto/verification/SasVerification.kt | 15 +----------- .../verification/VerificationRequest.kt | 6 ++--- .../verification/VerificationsProvider.kt | 2 +- 8 files changed, 25 insertions(+), 35 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/PrepareToEncryptUseCase.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/PrepareToEncryptUseCase.kt index 8765d99bca..e4c0469c74 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/PrepareToEncryptUseCase.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/PrepareToEncryptUseCase.kt @@ -100,10 +100,10 @@ internal class PrepareToEncryptUseCase @Inject constructor( var sharedKey = false val info = cryptoStore.getRoomCryptoInfo(roomId) - ?: throw java.lang.IllegalArgumentException("Encryption not configured in this room") + ?: throw java.lang.UnsupportedOperationException("Encryption not configured in this room") // how to react if this is null?? if (info.algorithm != MXCRYPTO_ALGORITHM_MEGOLM) { - throw java.lang.IllegalArgumentException("Unsupported algorithm ${info.algorithm}") + throw java.lang.UnsupportedOperationException("Unsupported algorithm ${info.algorithm}") } val settings = EncryptionSettings( algorithm = EventEncryptionAlgorithm.MEGOLM_V1_AES_SHA2, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/RustCryptoService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/RustCryptoService.kt index b3a3c35389..558b186cdd 100755 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/RustCryptoService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/RustCryptoService.kt @@ -225,7 +225,7 @@ internal class RustCryptoService @Inject constructor( } /** - * Tell if the MXCrypto is started + * Tell if the MXCrypto is started. * * @return true if the crypto is started */ @@ -288,7 +288,7 @@ internal class RustCryptoService @Inject constructor( } /** - * Close the crypto + * Close the crypto. */ override fun close() { cryptoCoroutineScope.coroutineContext.cancelChildren(CancellationException("Closing crypto module")) @@ -315,7 +315,7 @@ internal class RustCryptoService @Inject constructor( override fun crossSigningService() = crossSigningService /** - * A sync response has been received + * A sync response has been received. */ override suspend fun onSyncCompleted(syncResponse: SyncResponse, cryptoStoreAggregator: CryptoStoreAggregator) { if (isStarted()) { @@ -335,7 +335,7 @@ internal class RustCryptoService @Inject constructor( } /** - * Provides the device information for a user id and a device Id + * Provides the device information for a user id and a device Id. * * @param userId the user id * @param deviceId the device id @@ -387,7 +387,7 @@ internal class RustCryptoService @Inject constructor( * Configure a room to use encryption. * * @param roomId the room id to enable encryption in. - * @param algorithm the encryption config for the room. + * @param info the encryption config for the room. * @param membersId list of members to start tracking their devices * @return true if the operation succeeds. */ @@ -430,7 +430,7 @@ internal class RustCryptoService @Inject constructor( } /** - * Tells if a room is encrypted with MXCRYPTO_ALGORITHM_MEGOLM + * Tells if a room is encrypted with MXCRYPTO_ALGORITHM_MEGOLM. * * @param roomId the room id * @return true if the room is encrypted with algorithm MXCRYPTO_ALGORITHM_MEGOLM @@ -486,7 +486,7 @@ internal class RustCryptoService @Inject constructor( } /** - * Decrypt an event + * Decrypt an event. * * @param event the raw event. * @param timeline the id of the timeline where the event is decrypted. It is used to prevent replay attack. @@ -500,6 +500,7 @@ internal class RustCryptoService @Inject constructor( /** * Handle an m.room.encryption event. * + * @param roomId the roomId. * @param event the encryption event. */ private suspend fun onRoomEncryptionEvent(roomId: String, event: Event) { @@ -544,6 +545,7 @@ internal class RustCryptoService @Inject constructor( /** * Handle a change in the membership state of a member of a room. * + * @param roomId the roomId * @param event the membership event causing the change */ private suspend fun onRoomMembershipEvent(roomId: String, event: Event) { @@ -622,7 +624,8 @@ internal class RustCryptoService @Inject constructor( // Notify the our listeners about room keys so decryption is retried. toDeviceEvents.events.orEmpty().forEach { event -> - Timber.tag(loggerTag.value).d("[${myUserId.take(7)}|${deviceId}] Processed ToDevice event msgid:${event.toDeviceTracingId()} id:${event.eventId} type:${event.type}") + Timber.tag(loggerTag.value) + .d("[${myUserId.take(7)}|${deviceId}] Processed ToDevice event msgid:${event.toDeviceTracingId()} id:${event.eventId} type:${event.type}") if (event.getClearType() == EventType.ENCRYPTED) { // rust failed to decrypt it @@ -664,7 +667,7 @@ internal class RustCryptoService @Inject constructor( } /** - * Export the crypto keys + * Export the crypto keys. * * @param password the password * @return the exported keys @@ -679,7 +682,7 @@ internal class RustCryptoService @Inject constructor( } /** - * Import the room keys + * Import the room keys. * * @param roomKeysAsArray the room keys as array. * @param password the password diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/RustKeyBackupService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/RustKeyBackupService.kt index cffee25bc0..4796180cdc 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/RustKeyBackupService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/RustKeyBackupService.kt @@ -466,7 +466,7 @@ internal class RustKeyBackupService @Inject constructor( /** * Same method as [RoomKeysRestClient.getRoomKey] except that it accepts nullable - * parameters and always returns a KeysBackupData object through the Callback + * parameters and always returns a KeysBackupData object through the Callback. */ private suspend fun getKeys(sessionId: String?, roomId: String?, version: String): KeysBackupData { return when { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo022.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo022.kt index d0f612aa87..5d1119778d 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo022.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo022.kt @@ -23,7 +23,7 @@ import org.matrix.android.sdk.internal.util.database.RealmMigrator import java.io.File /** - * This migration creates the rust database and migrates from legacy crypto + * This migration creates the rust database and migrates from legacy crypto. */ internal class MigrateCryptoTo022( realm: DynamicRealm, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/RustVerificationService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/RustVerificationService.kt index 5a66f93b7c..ba769e52c0 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/RustVerificationService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/RustVerificationService.kt @@ -134,7 +134,7 @@ internal class RustVerificationService @Inject constructor( } } - /** Dispatch updates after a verification event has been received */ + /** Dispatch updates after a verification event has been received. */ private suspend fun onUpdate(event: Event) { Timber.v("[${olmMachine.userId().take(6)}] Verification on event ${event.getClearType()}") val sender = event.senderId ?: return @@ -320,7 +320,7 @@ internal class RustVerificationService @Inject constructor( override suspend fun startKeyVerification(method: VerificationMethod, otherUserId: String, requestId: String): String? { return if (method == VerificationMethod.SAS) { val request = olmMachine.getVerificationRequest(otherUserId, requestId) - ?: throw IllegalArgumentException("Unknown request with id: $requestId") + ?: throw UnsupportedOperationException("Unknown request with id: $requestId") val sas = request.startSasVerification() @@ -335,7 +335,7 @@ internal class RustVerificationService @Inject constructor( null } } else { - throw IllegalArgumentException("Unknown verification method") + throw UnsupportedOperationException("Unknown verification method") } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/SasVerification.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/SasVerification.kt index 13afdd7b5b..9c8e327cd5 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/SasVerification.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/SasVerification.kt @@ -133,7 +133,7 @@ internal class SasVerification @AssistedInject constructor( override val method: VerificationMethod get() = VerificationMethod.QR_CODE_SCAN - /** Is this verification happening over to-device messages */ + /** Is this verification happening over to-device messages. */ override fun isToDeviceTransport(): Boolean = inner.roomId() == null // /** Does the verification flow support showing emojis as the short auth string */ @@ -224,19 +224,6 @@ internal class SasVerification @AssistedInject constructor( verificationListenersHolder.dispatchTxUpdated(this@SasVerification) } - /** Fetch fresh data from the Rust side for our verification flow */ -// private fun refreshData() { -// when (val verification = innerMachine.getVerification(inner.otherUserId, inner.flowId)) { -// is Verification.SasV1 -> { -// inner = verification.sas -// } -// else -> { -// } -// } -// -// return -// } - override fun onChange(state: SasState) { innerState = state verificationListenersHolder.dispatchTxUpdated(this) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationRequest.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationRequest.kt index 641bf66c12..cded4d5961 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationRequest.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationRequest.kt @@ -103,7 +103,7 @@ internal class VerificationRequest @AssistedInject constructor( fun innerState() = innerVerificationRequest.state() - /** The user ID of the other user that is participating in this verification flow */ + /** The user ID of the other user that is participating in this verification flow. */ internal fun otherUser(): String { return innerVerificationRequest.otherUserId() } @@ -117,7 +117,7 @@ internal class VerificationRequest @AssistedInject constructor( return innerVerificationRequest.otherDeviceId() } - /** Did we initiate this verification flow */ + /** Did we initiate this verification flow. */ internal fun weStarted(): Boolean { return innerVerificationRequest.weStarted() } @@ -140,7 +140,7 @@ internal class VerificationRequest @AssistedInject constructor( // return innerVerificationRequest.isReady() // } - /** Did we advertise that we're able to scan QR codes */ + /** Did we advertise that we're able to scan QR codes. */ internal fun canScanQrCodes(): Boolean { return innerVerificationRequest.ourSupportedMethods()?.contains(VERIFICATION_METHOD_QR_CODE_SCAN) ?: false } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationsProvider.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationsProvider.kt index 7544368ef7..35d81dec70 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationsProvider.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationsProvider.kt @@ -36,7 +36,7 @@ internal class VerificationsProvider @Inject constructor( return innerMachine.getVerificationRequests(userId).map(verificationRequestFactory::create) } - /** Get a verification request for the given user with the given flow ID */ + /** Get a verification request for the given user with the given flow ID. */ fun getVerificationRequest(userId: String, flowId: String): VerificationRequest? { return innerMachine.getVerificationRequest(userId, flowId)?.let { innerVerificationRequest -> verificationRequestFactory.create(innerVerificationRequest) From e8922a5fa74c1712f73cf7dc6a4a11684014afcd Mon Sep 17 00:00:00 2001 From: Valere Date: Wed, 11 Oct 2023 12:13:50 +0200 Subject: [PATCH 13/61] property not in schema --- .../vector/app/features/analytics/impl/DefaultVectorAnalytics.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/vector/src/main/java/im/vector/app/features/analytics/impl/DefaultVectorAnalytics.kt b/vector/src/main/java/im/vector/app/features/analytics/impl/DefaultVectorAnalytics.kt index 78a87b38b0..2a7d0ac975 100644 --- a/vector/src/main/java/im/vector/app/features/analytics/impl/DefaultVectorAnalytics.kt +++ b/vector/src/main/java/im/vector/app/features/analytics/impl/DefaultVectorAnalytics.kt @@ -214,7 +214,6 @@ class DefaultVectorAnalytics @Inject constructor( private fun Map.toPostHogUserProperties(): Properties { return Properties().apply { putAll(this@toPostHogUserProperties.filter { it.value != null }) - put("crypto", "rust") } } From cd101f871cae9c2390194a94e94128b674681129 Mon Sep 17 00:00:00 2001 From: Valere Date: Wed, 11 Oct 2023 15:07:48 +0200 Subject: [PATCH 14/61] missing mock --- .../app/features/settings/devices/v2/DevicesViewModelTest.kt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/DevicesViewModelTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/DevicesViewModelTest.kt index c2a14d8c39..4f407faa70 100644 --- a/vector/src/test/java/im/vector/app/features/settings/devices/v2/DevicesViewModelTest.kt +++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/DevicesViewModelTest.kt @@ -56,6 +56,7 @@ import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo import org.matrix.android.sdk.api.session.crypto.model.DeviceInfo import org.matrix.android.sdk.api.session.crypto.model.RoomEncryptionTrustLevel import org.matrix.android.sdk.api.session.crypto.verification.VerificationTransaction +import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilities import org.matrix.android.sdk.api.session.uia.DefaultBaseAuth private const val A_CURRENT_DEVICE_ID = "current-device-id" @@ -107,6 +108,9 @@ class DevicesViewModelTest { givenVerificationService() givenCurrentSessionCrossSigningInfo() givenDeviceFullInfoList(deviceId1 = A_DEVICE_ID_1, deviceId2 = A_DEVICE_ID_2) + fakeActiveSessionHolder.fakeSession.fakeHomeServerCapabilitiesService.givenCapabilities( + HomeServerCapabilities() + ) fakeVectorPreferences.givenSessionManagerShowIpAddress(false) } From 5f68f98d3a327256f44e06be5480c8d2ba36c79f Mon Sep 17 00:00:00 2001 From: Valere Date: Wed, 11 Oct 2023 17:04:05 +0200 Subject: [PATCH 15/61] fix unit test --- .../settings/devices/v2/RefreshDevicesUseCaseTest.kt | 2 +- .../devices/v2/othersessions/OtherSessionsViewModelTest.kt | 7 +++++-- .../devices/v2/overview/SessionOverviewViewModelTest.kt | 4 ++++ .../settings/devices/v2/rename/RenameSessionUseCaseTest.kt | 2 +- 4 files changed, 11 insertions(+), 4 deletions(-) diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/RefreshDevicesUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/RefreshDevicesUseCaseTest.kt index 047ae28be7..a8dee9f178 100644 --- a/vector/src/test/java/im/vector/app/features/settings/devices/v2/RefreshDevicesUseCaseTest.kt +++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/RefreshDevicesUseCaseTest.kt @@ -35,7 +35,7 @@ class RefreshDevicesUseCaseTest { fun `given current session when refreshing then devices list and keys are fetched`() { val session = fakeActiveSessionHolder.fakeSession coEvery { session.cryptoService().fetchDevicesList() } returns emptyList() - coEvery { session.cryptoService().downloadKeysIfNeeded(any()) } returns MXUsersDevicesMap() + coEvery { session.cryptoService().downloadKeysIfNeeded(any(), any()) } returns MXUsersDevicesMap() runBlocking { refreshDevicesUseCase.execute() diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModelTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModelTest.kt index 8b595d0ac5..ab4e1a222b 100644 --- a/vector/src/test/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModelTest.kt +++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModelTest.kt @@ -46,6 +46,7 @@ import org.junit.Before import org.junit.Ignore import org.junit.Rule import org.junit.Test +import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilities import org.matrix.android.sdk.api.session.uia.DefaultBaseAuth private const val A_DEVICE_ID_1 = "device-id-1" @@ -87,8 +88,10 @@ class OtherSessionsViewModelTest { // Needed for internal usage of Flow.throttleFirst() inside the ViewModel mockkStatic(SystemClock::class) every { SystemClock.elapsedRealtime() } returns 1234 - - givenVerificationService() + fakeActiveSessionHolder.fakeSession.fakeHomeServerCapabilitiesService.givenCapabilities( + HomeServerCapabilities() + ) + givenVerificationService().givenEventFlow() fakeVectorPreferences.givenSessionManagerShowIpAddress(false) } diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModelTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModelTest.kt index ab99f71588..8fe68bdbf6 100644 --- a/vector/src/test/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModelTest.kt +++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModelTest.kt @@ -52,6 +52,7 @@ import org.junit.Ignore import org.junit.Rule import org.junit.Test import org.matrix.android.sdk.api.session.crypto.model.RoomEncryptionTrustLevel +import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilities import org.matrix.android.sdk.api.session.uia.DefaultBaseAuth private const val A_SESSION_ID_1 = "session-id-1" @@ -101,6 +102,9 @@ class SessionOverviewViewModelTest { mockkStatic(SystemClock::class) every { SystemClock.elapsedRealtime() } returns 1234 + fakeActiveSessionHolder.fakeSession.fakeHomeServerCapabilitiesService.givenCapabilities( + HomeServerCapabilities() + ) givenVerificationService() fakeGetNotificationsStatusUseCase.givenExecuteReturns( fakeActiveSessionHolder.fakeSession, diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/rename/RenameSessionUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/rename/RenameSessionUseCaseTest.kt index 92ef81e56b..d52504e80e 100644 --- a/vector/src/test/java/im/vector/app/features/settings/devices/v2/rename/RenameSessionUseCaseTest.kt +++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/rename/RenameSessionUseCaseTest.kt @@ -43,7 +43,7 @@ class RenameSessionUseCaseTest { fun `given a device id and a new name when no error during rename then the device is renamed with success`() = runTest { // Given fakeActiveSessionHolder.fakeSession.fakeCryptoService.givenSetDeviceNameSucceeds() - coVerify { refreshDevicesUseCase.execute() } + coEvery { refreshDevicesUseCase.execute() } returns Unit // When val result = renameSessionUseCase.execute(A_DEVICE_ID, A_DEVICE_NAME) From 491f52a3a30fc0a0a0352dda1f2efde43d253613 Mon Sep 17 00:00:00 2001 From: Valere Date: Wed, 11 Oct 2023 17:57:59 +0200 Subject: [PATCH 16/61] dead code --- .../android/sdk/internal/crypto/E2eeShareKeysHistoryTest.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/E2eeShareKeysHistoryTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/E2eeShareKeysHistoryTest.kt index fc1b5bba93..67cde3e3fc 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/E2eeShareKeysHistoryTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/E2eeShareKeysHistoryTest.kt @@ -197,7 +197,6 @@ class E2eeShareKeysHistoryTest : InstrumentedTest { @Test fun testNeedsRotationFromSharedToWorldReadable() { - Assume.assumeTrue("Test is flacky on legacy crypto", BuildConfig.FLAVOR == "rustCrypto") testRotationDueToVisibilityChange(RoomHistoryVisibility.SHARED, RoomHistoryVisibilityContent("world_readable")) } From 77a576784f4de8007965739d331d466a2759c40e Mon Sep 17 00:00:00 2001 From: Valere Date: Wed, 11 Oct 2023 23:08:35 +0200 Subject: [PATCH 17/61] fix test compilation --- .../java/org/matrix/android/sdk/common/CryptoTestHelper.kt | 2 +- .../android/sdk/internal/crypto/E2eeShareKeysHistoryTest.kt | 2 -- .../android/sdk/internal/crypto/keysbackup/KeysBackupTest.kt | 4 ++-- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CryptoTestHelper.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CryptoTestHelper.kt index 4b9c817e5c..c1bd5da6ac 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CryptoTestHelper.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CryptoTestHelper.kt @@ -261,7 +261,7 @@ class CryptoTestHelper(val testHelper: CommonTestHelper) { return MegolmBackupCreationInfo( algorithm = MXCRYPTO_ALGORITHM_MEGOLM_BACKUP, authData = createFakeMegolmBackupAuthData(), - recoveryKey = BackupUtils.recoveryKeyFromPassphrase("3cnTdW")!! + recoveryKey = BackupUtils.recoveryKeyFromPassphrase("3cnTdW") ) } diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/E2eeShareKeysHistoryTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/E2eeShareKeysHistoryTest.kt index 67cde3e3fc..095d88545b 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/E2eeShareKeysHistoryTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/E2eeShareKeysHistoryTest.kt @@ -28,14 +28,12 @@ import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.JUnit4 import org.junit.runners.MethodSorters -import org.matrix.android.sdk.BuildConfig import org.matrix.android.sdk.InstrumentedTest import org.matrix.android.sdk.api.query.QueryStringValue import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.toContent import org.matrix.android.sdk.api.session.events.model.toModel -import org.matrix.android.sdk.api.session.getRoom import org.matrix.android.sdk.api.session.room.Room import org.matrix.android.sdk.api.session.room.failure.JoinRoomFailure import org.matrix.android.sdk.api.session.room.model.Membership diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupTest.kt index 50e8972327..485dcd68b5 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupTest.kt @@ -542,7 +542,7 @@ class KeysBackupTest : InstrumentedTest { assertFails { testData.aliceSession2.cryptoService().keysBackupService().trustKeysBackupVersionWithRecoveryKey( testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion!!, - BackupUtils.recoveryKeyFromPassphrase("Bad recovery key")!!, + BackupUtils.recoveryKeyFromPassphrase("Bad recovery key"), ) } @@ -680,7 +680,7 @@ class KeysBackupTest : InstrumentedTest { assertFailsWith { keysBackupService.restoreKeysWithRecoveryKey( keysBackupService.keysBackupVersion!!, - BackupUtils.recoveryKeyFromBase58("EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d")!!, + BackupUtils.recoveryKeyFromBase58("EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d"), null, null, null, From d4c141664b06c8a5870106c8cc17a543b453d7b9 Mon Sep 17 00:00:00 2001 From: Valere Date: Thu, 12 Oct 2023 09:12:32 +0200 Subject: [PATCH 18/61] fix ignored test --- .../devices/v2/DevicesViewModelTest.kt | 36 +++++------------ .../OtherSessionsViewModelTest.kt | 40 +++++-------------- .../overview/SessionOverviewViewModelTest.kt | 17 -------- 3 files changed, 20 insertions(+), 73 deletions(-) diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/DevicesViewModelTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/DevicesViewModelTest.kt index 4f407faa70..27e9fd86ce 100644 --- a/vector/src/test/java/im/vector/app/features/settings/devices/v2/DevicesViewModelTest.kt +++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/DevicesViewModelTest.kt @@ -48,7 +48,6 @@ import kotlinx.coroutines.flow.flowOf import org.amshove.kluent.shouldBeEqualTo import org.junit.After import org.junit.Before -import org.junit.Ignore import org.junit.Rule import org.junit.Test import org.matrix.android.sdk.api.session.crypto.crosssigning.DeviceTrustLevel @@ -129,34 +128,17 @@ class DevicesViewModelTest { } @Test - @Ignore fun `given the viewModel when initializing it then verification listener is added`() { -// // Given -// val fakeVerificationService = givenVerificationService() -// -// // When -// val viewModel = createViewModel() -// -// // Then -// verify { -// fakeVerificationService.addListener(viewModel) -// } - } + // Given + val fakeVerificationService = givenVerificationService() - @Test - @Ignore - fun `given the viewModel when clearing it then verification listener is removed`() { -// // Given -// val fakeVerificationService = givenVerificationService() -// -// // When -// val viewModel = createViewModel() -// viewModel.onCleared() -// -// // Then -// verify { -// fakeVerificationService.removeListener(viewModel) -// } + // When + createViewModel() + + // Then + verify { + fakeVerificationService.requestEventFlow() + } } @Test diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModelTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModelTest.kt index ab4e1a222b..6c0ef9f14c 100644 --- a/vector/src/test/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModelTest.kt +++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModelTest.kt @@ -38,12 +38,12 @@ import io.mockk.justRun import io.mockk.mockk import io.mockk.mockkStatic import io.mockk.unmockkAll +import io.mockk.verify import io.mockk.verifyAll import kotlinx.coroutines.flow.flowOf import org.amshove.kluent.shouldBeEqualTo import org.junit.After import org.junit.Before -import org.junit.Ignore import org.junit.Rule import org.junit.Test import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilities @@ -108,38 +108,20 @@ class OtherSessionsViewModelTest { } @Test - @Ignore fun `given the viewModel when initializing it then verification listener is added`() { // Given -// val fakeVerificationService = givenVerificationService() -// val devices = mockk>() -// givenGetDeviceFullInfoListReturns(filterType = defaultArgs.defaultFilter, devices) -// -// // When -// val viewModel = createViewModel() + val fakeVerificationService = givenVerificationService() + .also { it.givenEventFlow() } + val devices = mockk>() + givenGetDeviceFullInfoListReturns(filterType = defaultArgs.defaultFilter, devices) - // Then -// verify { -// fakeVerificationService.addListener(viewModel) -// } - } + // When + createViewModel() - @Test - @Ignore - fun `given the viewModel when clearing it then verification listener is removed`() { -// // Given -// val fakeVerificationService = givenVerificationService() -// val devices = mockk>() -// givenGetDeviceFullInfoListReturns(filterType = defaultArgs.defaultFilter, devices) -// -// // When -// val viewModel = createViewModel() -// viewModel.onCleared() -// -// // Then -// verify { -// fakeVerificationService.removeListener(viewModel) -// } + // Then + verify { + fakeVerificationService.requestEventFlow() + } } @Test diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModelTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModelTest.kt index 8fe68bdbf6..95adbb842a 100644 --- a/vector/src/test/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModelTest.kt +++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModelTest.kt @@ -48,7 +48,6 @@ import kotlinx.coroutines.flow.flowOf import org.amshove.kluent.shouldBeEqualTo import org.junit.After import org.junit.Before -import org.junit.Ignore import org.junit.Rule import org.junit.Test import org.matrix.android.sdk.api.session.crypto.model.RoomEncryptionTrustLevel @@ -142,22 +141,6 @@ class SessionOverviewViewModelTest { } } - @Test - @Ignore - fun `given the viewModel when clearing it then verification listener is removed`() { -// // Given -// val fakeVerificationService = givenVerificationService() -// -// // When -// val viewModel = createViewModel() -// viewModel.onCleared() -// -// // Then -// verify { -// fakeVerificationService.removeListener(viewModel) -// } - } - @Test fun `given the viewModel has been initialized then pushers are refreshed`() { createViewModel() From d4c6a46e90d5e1b6f90b2d2e79e063c5ca9754ac Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 12 Oct 2023 18:12:49 +0200 Subject: [PATCH 19/61] Parse "io.element.disable_network_constraint" from login response, to ensure that the config is retrieved. The add pusher worker can be configured before the .well-known config is retrieved. --- .../sdk/api/auth/data/DiscoveryInformation.kt | 8 ++++- .../sdk/internal/di/WorkManagerProvider.kt | 9 +++-- .../sdk/internal/session/SessionModule.kt | 5 +++ .../session/pushers/DefaultPushersService.kt | 6 ++-- .../session/room/send/DefaultSendService.kt | 5 +-- .../timeline/TimelineSendEventWorkCommon.kt | 5 +-- .../session/sync/DefaultSyncService.kt | 7 ++-- .../internal/session/sync/job/SyncWorker.kt | 15 ++++---- .../workmanager/DefaultWorkManagerConfig.kt | 35 +++++++++++++++++++ .../session/workmanager/WorkManagerConfig.kt | 21 +++++++++++ .../pushers/DefaultPushersServiceTest.kt | 6 ++-- .../matrix/android/sdk/test/fakes/FakeWork.kt | 20 +++++++++++ .../sdk/test/fakes/FakeWorkManagerConfig.kt | 25 +++++++++++++ 13 files changed, 141 insertions(+), 26 deletions(-) create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/workmanager/DefaultWorkManagerConfig.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/workmanager/WorkManagerConfig.kt create mode 100644 matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeWork.kt create mode 100644 matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeWorkManagerConfig.kt diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/DiscoveryInformation.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/DiscoveryInformation.kt index 384dcdce45..94390e2ffc 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/DiscoveryInformation.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/DiscoveryInformation.kt @@ -36,5 +36,11 @@ data class DiscoveryInformation( * Note: matrix.org does not send this field */ @Json(name = "m.identity_server") - val identityServer: WellKnownBaseConfig? = null + val identityServer: WellKnownBaseConfig? = null, + + /** + * If set to true, the SDK will not use the network constraint when configuring Worker for the WorkManager. + */ + @Json(name = "io.element.disable_network_constraint") + val disableNetworkConstraint: Boolean? = null, ) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/WorkManagerProvider.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/WorkManagerProvider.kt index 1852f05ce0..1728fe9f69 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/WorkManagerProvider.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/WorkManagerProvider.kt @@ -29,9 +29,8 @@ import androidx.work.WorkRequest import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import org.matrix.android.sdk.api.MatrixCoroutineDispatchers -import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.internal.session.SessionScope -import org.matrix.android.sdk.internal.session.homeserver.HomeServerCapabilitiesDataSource +import org.matrix.android.sdk.internal.session.workmanager.WorkManagerConfig import org.matrix.android.sdk.internal.worker.MatrixWorkerFactory import timber.log.Timber import java.util.concurrent.TimeUnit @@ -106,12 +105,12 @@ internal class WorkManagerProvider @Inject constructor( private const val MATRIX_SDK_TAG_PREFIX = "MatrixSDK-" fun getWorkConstraints( - homeServerCapabilitiesDataSource: HomeServerCapabilitiesDataSource, + workManagerConfig: WorkManagerConfig, ): Constraints { - val withNetworkConstraint = homeServerCapabilitiesDataSource.getHomeServerCapabilities()?.disableNetworkConstraint.orFalse().not() + val widthNetworkConstraint = workManagerConfig.withNetworkConstraint() return Constraints.Builder() .apply { - if (withNetworkConstraint) { + if (widthNetworkConstraint) { setRequiredNetworkType(NetworkType.CONNECTED) } else { Timber.w("Network constraint is disabled") diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionModule.kt index 4e778e04cf..54834f4263 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionModule.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionModule.kt @@ -97,6 +97,8 @@ import org.matrix.android.sdk.internal.session.room.tombstone.RoomTombstoneEvent import org.matrix.android.sdk.internal.session.typing.DefaultTypingUsersTracker import org.matrix.android.sdk.internal.session.user.accountdata.DefaultSessionAccountDataService import org.matrix.android.sdk.internal.session.widgets.DefaultWidgetURLFormatter +import org.matrix.android.sdk.internal.session.workmanager.DefaultWorkManagerConfig +import org.matrix.android.sdk.internal.session.workmanager.WorkManagerConfig import retrofit2.Retrofit import java.io.File import javax.inject.Provider @@ -422,4 +424,7 @@ internal abstract class SessionModule { @Binds abstract fun bindPollAggregationProcessor(processor: DefaultPollAggregationProcessor): PollAggregationProcessor + + @Binds + abstract fun bindWorkManaerConfig(config: DefaultWorkManagerConfig): WorkManagerConfig } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/DefaultPushersService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/DefaultPushersService.kt index 0d73d79662..690a6dd711 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/DefaultPushersService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/DefaultPushersService.kt @@ -27,8 +27,8 @@ import org.matrix.android.sdk.internal.database.query.where import org.matrix.android.sdk.internal.di.SessionDatabase import org.matrix.android.sdk.internal.di.SessionId import org.matrix.android.sdk.internal.di.WorkManagerProvider -import org.matrix.android.sdk.internal.session.homeserver.HomeServerCapabilitiesDataSource import org.matrix.android.sdk.internal.session.pushers.gateway.PushGatewayNotifyTask +import org.matrix.android.sdk.internal.session.workmanager.WorkManagerConfig import org.matrix.android.sdk.internal.task.TaskExecutor import org.matrix.android.sdk.internal.task.configureWith import org.matrix.android.sdk.internal.worker.WorkerParamsFactory @@ -46,7 +46,7 @@ internal class DefaultPushersService @Inject constructor( private val togglePusherTask: TogglePusherTask, private val removePusherTask: RemovePusherTask, private val taskExecutor: TaskExecutor, - private val homeServerCapabilitiesDataSource: HomeServerCapabilitiesDataSource, + private val workManagerConfig: WorkManagerConfig, ) : PushersService { override suspend fun testPush( @@ -132,7 +132,7 @@ internal class DefaultPushersService @Inject constructor( private fun enqueueAddPusher(pusher: JsonPusher): UUID { val params = AddPusherWorker.Params(sessionId, pusher) val request = workManagerProvider.matrixOneTimeWorkRequestBuilder() - .setConstraints(WorkManagerProvider.getWorkConstraints(homeServerCapabilitiesDataSource)) + .setConstraints(WorkManagerProvider.getWorkConstraints(workManagerConfig)) .setInputData(WorkerParamsFactory.toData(params)) .setBackoffCriteria(BackoffPolicy.LINEAR, WorkManagerProvider.BACKOFF_DELAY_MILLIS, TimeUnit.MILLISECONDS) .build() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt index dd2d79741a..4b2610c033 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt @@ -55,6 +55,7 @@ import org.matrix.android.sdk.internal.di.WorkManagerProvider import org.matrix.android.sdk.internal.session.content.UploadContentWorker import org.matrix.android.sdk.internal.session.homeserver.HomeServerCapabilitiesDataSource import org.matrix.android.sdk.internal.session.room.send.queue.EventSenderProcessor +import org.matrix.android.sdk.internal.session.workmanager.WorkManagerConfig import org.matrix.android.sdk.internal.task.TaskExecutor import org.matrix.android.sdk.internal.util.CancelableWork import org.matrix.android.sdk.internal.worker.WorkerParamsFactory @@ -75,7 +76,7 @@ internal class DefaultSendService @AssistedInject constructor( private val localEchoRepository: LocalEchoRepository, private val eventSenderProcessor: EventSenderProcessor, private val cancelSendTracker: CancelSendTracker, - private val homeServerCapabilitiesDataSource: HomeServerCapabilitiesDataSource, + private val workManagerConfig: WorkManagerConfig, ) : SendService { @AssistedFactory @@ -375,7 +376,7 @@ internal class DefaultSendService @AssistedInject constructor( val uploadWorkData = WorkerParamsFactory.toData(uploadMediaWorkerParams) return workManagerProvider.matrixOneTimeWorkRequestBuilder() - .setConstraints(WorkManagerProvider.getWorkConstraints(homeServerCapabilitiesDataSource)) + .setConstraints(WorkManagerProvider.getWorkConstraints(workManagerConfig)) .startChain(true) .setInputData(uploadWorkData) .setBackoffCriteria(BackoffPolicy.LINEAR, WorkManagerProvider.BACKOFF_DELAY_MILLIS, TimeUnit.MILLISECONDS) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineSendEventWorkCommon.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineSendEventWorkCommon.kt index 0295a857fe..ec52cfb2e2 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineSendEventWorkCommon.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineSendEventWorkCommon.kt @@ -23,6 +23,7 @@ import androidx.work.OneTimeWorkRequest import org.matrix.android.sdk.api.util.Cancelable import org.matrix.android.sdk.internal.di.WorkManagerProvider import org.matrix.android.sdk.internal.session.homeserver.HomeServerCapabilitiesDataSource +import org.matrix.android.sdk.internal.session.workmanager.WorkManagerConfig import org.matrix.android.sdk.internal.util.CancelableWork import org.matrix.android.sdk.internal.worker.startChain import java.util.concurrent.TimeUnit @@ -36,7 +37,7 @@ import javax.inject.Inject */ internal class TimelineSendEventWorkCommon @Inject constructor( private val workManagerProvider: WorkManagerProvider, - private val homeServerCapabilitiesDataSource: HomeServerCapabilitiesDataSource, + private val workManagerConfig: WorkManagerConfig, ) { fun postWork(roomId: String, workRequest: OneTimeWorkRequest, policy: ExistingWorkPolicy = ExistingWorkPolicy.APPEND_OR_REPLACE): Cancelable { @@ -49,7 +50,7 @@ internal class TimelineSendEventWorkCommon @Inject constructor( inline fun createWork(data: Data, startChain: Boolean): OneTimeWorkRequest { return workManagerProvider.matrixOneTimeWorkRequestBuilder() - .setConstraints(WorkManagerProvider.getWorkConstraints(homeServerCapabilitiesDataSource)) + .setConstraints(WorkManagerProvider.getWorkConstraints(workManagerConfig)) .startChain(startChain) .setInputData(data) .setBackoffCriteria(BackoffPolicy.LINEAR, WorkManagerProvider.BACKOFF_DELAY_MILLIS, TimeUnit.MILLISECONDS) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/DefaultSyncService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/DefaultSyncService.kt index 5c3ca5a6b1..280b8e6c21 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/DefaultSyncService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/DefaultSyncService.kt @@ -23,6 +23,7 @@ import org.matrix.android.sdk.internal.session.SessionState import org.matrix.android.sdk.internal.session.homeserver.HomeServerCapabilitiesDataSource import org.matrix.android.sdk.internal.session.sync.job.SyncThread import org.matrix.android.sdk.internal.session.sync.job.SyncWorker +import org.matrix.android.sdk.internal.session.workmanager.WorkManagerConfig import timber.log.Timber import javax.inject.Inject import javax.inject.Provider @@ -34,7 +35,7 @@ internal class DefaultSyncService @Inject constructor( private val syncTokenStore: SyncTokenStore, private val syncRequestStateTracker: SyncRequestStateTracker, private val sessionState: SessionState, - private val homeServerCapabilitiesDataSource: HomeServerCapabilitiesDataSource, + private val workManagerConfig: WorkManagerConfig, ) : SyncService { private var syncThread: SyncThread? = null @@ -42,7 +43,7 @@ internal class DefaultSyncService @Inject constructor( SyncWorker.requireBackgroundSync( workManagerProvider = workManagerProvider, sessionId = sessionId, - homeServerCapabilitiesDataSource = homeServerCapabilitiesDataSource, + workManagerConfig = workManagerConfig, ) } @@ -50,7 +51,7 @@ internal class DefaultSyncService @Inject constructor( SyncWorker.automaticallyBackgroundSync( workManagerProvider = workManagerProvider, sessionId = sessionId, - homeServerCapabilitiesDataSource = homeServerCapabilitiesDataSource, + workManagerConfig = workManagerConfig, serverTimeoutInSeconds = timeOutInSeconds, delayInSeconds = repeatDelayInSeconds, ) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/job/SyncWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/job/SyncWorker.kt index 15002ed43a..7a6b929164 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/job/SyncWorker.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/job/SyncWorker.kt @@ -28,6 +28,7 @@ import org.matrix.android.sdk.internal.session.SessionComponent import org.matrix.android.sdk.internal.session.homeserver.HomeServerCapabilitiesDataSource import org.matrix.android.sdk.internal.session.sync.SyncPresence import org.matrix.android.sdk.internal.session.sync.SyncTask +import org.matrix.android.sdk.internal.session.workmanager.WorkManagerConfig import org.matrix.android.sdk.internal.worker.SessionSafeCoroutineWorker import org.matrix.android.sdk.internal.worker.SessionWorkerParams import org.matrix.android.sdk.internal.worker.WorkerParamsFactory @@ -60,7 +61,7 @@ internal class SyncWorker(context: Context, workerParameters: WorkerParameters, @Inject lateinit var syncTask: SyncTask @Inject lateinit var workManagerProvider: WorkManagerProvider - @Inject lateinit var homeServerCapabilitiesDataSource: HomeServerCapabilitiesDataSource + @Inject lateinit var workManagerConfig: WorkManagerConfig override fun injectWith(injector: SessionComponent) { injector.inject(this) @@ -79,7 +80,7 @@ internal class SyncWorker(context: Context, workerParameters: WorkerParameters, automaticallyBackgroundSync( workManagerProvider = workManagerProvider, sessionId = params.sessionId, - homeServerCapabilitiesDataSource = homeServerCapabilitiesDataSource, + workManagerConfig = workManagerConfig, serverTimeoutInSeconds = params.timeout, delayInSeconds = params.delay, forceImmediate = hasToDeviceEvents @@ -89,7 +90,7 @@ internal class SyncWorker(context: Context, workerParameters: WorkerParameters, requireBackgroundSync( workManagerProvider = workManagerProvider, sessionId = params.sessionId, - homeServerCapabilitiesDataSource = homeServerCapabilitiesDataSource, + workManagerConfig = workManagerConfig, serverTimeoutInSeconds = 0 ) } @@ -127,7 +128,7 @@ internal class SyncWorker(context: Context, workerParameters: WorkerParameters, fun requireBackgroundSync( workManagerProvider: WorkManagerProvider, sessionId: String, - homeServerCapabilitiesDataSource: HomeServerCapabilitiesDataSource, + workManagerConfig: WorkManagerConfig, serverTimeoutInSeconds: Long = 0 ) { val data = WorkerParamsFactory.toData( @@ -139,7 +140,7 @@ internal class SyncWorker(context: Context, workerParameters: WorkerParameters, ) ) val workRequest = workManagerProvider.matrixOneTimeWorkRequestBuilder() - .setConstraints(WorkManagerProvider.getWorkConstraints(homeServerCapabilitiesDataSource)) + .setConstraints(WorkManagerProvider.getWorkConstraints(workManagerConfig)) .setBackoffCriteria(BackoffPolicy.LINEAR, WorkManagerProvider.BACKOFF_DELAY_MILLIS, TimeUnit.MILLISECONDS) .setInputData(data) .startChain(true) @@ -151,7 +152,7 @@ internal class SyncWorker(context: Context, workerParameters: WorkerParameters, fun automaticallyBackgroundSync( workManagerProvider: WorkManagerProvider, sessionId: String, - homeServerCapabilitiesDataSource: HomeServerCapabilitiesDataSource, + workManagerConfig: WorkManagerConfig, serverTimeoutInSeconds: Long = 0, delayInSeconds: Long = 30, forceImmediate: Boolean = false @@ -166,7 +167,7 @@ internal class SyncWorker(context: Context, workerParameters: WorkerParameters, ) ) val workRequest = workManagerProvider.matrixOneTimeWorkRequestBuilder() - .setConstraints(WorkManagerProvider.getWorkConstraints(homeServerCapabilitiesDataSource)) + .setConstraints(WorkManagerProvider.getWorkConstraints(workManagerConfig)) .setInputData(data) .setBackoffCriteria(BackoffPolicy.LINEAR, WorkManagerProvider.BACKOFF_DELAY_MILLIS, TimeUnit.MILLISECONDS) .setInitialDelay(if (forceImmediate) 0 else delayInSeconds, TimeUnit.SECONDS) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/workmanager/DefaultWorkManagerConfig.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/workmanager/DefaultWorkManagerConfig.kt new file mode 100644 index 0000000000..aa3fd718cd --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/workmanager/DefaultWorkManagerConfig.kt @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.workmanager + +import org.matrix.android.sdk.api.auth.data.Credentials +import org.matrix.android.sdk.api.extensions.orFalse +import org.matrix.android.sdk.internal.session.homeserver.HomeServerCapabilitiesDataSource +import javax.inject.Inject + +internal class DefaultWorkManagerConfig @Inject constructor( + private val credentials: Credentials, + private val homeServerCapabilitiesDataSource: HomeServerCapabilitiesDataSource, +) : WorkManagerConfig { + override fun withNetworkConstraint(): Boolean { + return if (credentials.discoveryInformation?.disableNetworkConstraint == true) { + false + } else { + homeServerCapabilitiesDataSource.getHomeServerCapabilities()?.disableNetworkConstraint.orFalse().not() + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/workmanager/WorkManagerConfig.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/workmanager/WorkManagerConfig.kt new file mode 100644 index 0000000000..6e9f2459ad --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/workmanager/WorkManagerConfig.kt @@ -0,0 +1,21 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.workmanager + +internal interface WorkManagerConfig { + fun withNetworkConstraint(): Boolean +} diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/pushers/DefaultPushersServiceTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/pushers/DefaultPushersServiceTest.kt index 47d3dc2085..5037063833 100644 --- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/pushers/DefaultPushersServiceTest.kt +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/pushers/DefaultPushersServiceTest.kt @@ -21,11 +21,11 @@ import kotlinx.coroutines.test.runTest import org.junit.Test import org.matrix.android.sdk.test.fakes.FakeAddPusherTask import org.matrix.android.sdk.test.fakes.FakeGetPushersTask -import org.matrix.android.sdk.test.fakes.FakeHomeServerCapabilitiesDataSource import org.matrix.android.sdk.test.fakes.FakeMonarchy import org.matrix.android.sdk.test.fakes.FakeRemovePusherTask import org.matrix.android.sdk.test.fakes.FakeTaskExecutor import org.matrix.android.sdk.test.fakes.FakeTogglePusherTask +import org.matrix.android.sdk.test.fakes.FakeWorkManagerConfig import org.matrix.android.sdk.test.fakes.FakeWorkManagerProvider import org.matrix.android.sdk.test.fakes.internal.FakePushGatewayNotifyTask import org.matrix.android.sdk.test.fixtures.PusherFixture @@ -42,7 +42,7 @@ class DefaultPushersServiceTest { private val togglePusherTask = FakeTogglePusherTask() private val removePusherTask = FakeRemovePusherTask() private val taskExecutor = FakeTaskExecutor() - private val homeServerCapabilitiesDataSource = FakeHomeServerCapabilitiesDataSource() + private val fakeWorkManagerConfig = FakeWorkManagerConfig() private val pushersService = DefaultPushersService( workManagerProvider.instance, @@ -54,7 +54,7 @@ class DefaultPushersServiceTest { togglePusherTask, removePusherTask, taskExecutor.instance, - homeServerCapabilitiesDataSource.instance, + fakeWorkManagerConfig, ) @Test diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeWork.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeWork.kt new file mode 100644 index 0000000000..0d1c478663 --- /dev/null +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeWork.kt @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * 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. + */ + +package org.matrix.android.sdk.test.fakes + +class FakeWork { +} diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeWorkManagerConfig.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeWorkManagerConfig.kt new file mode 100644 index 0000000000..457bf29db0 --- /dev/null +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeWorkManagerConfig.kt @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * 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. + */ + +package org.matrix.android.sdk.test.fakes + +import org.matrix.android.sdk.internal.session.workmanager.WorkManagerConfig + +class FakeWorkManagerConfig : WorkManagerConfig { + override fun withNetworkConstraint(): Boolean { + return true + } +} From 7884b9dd5e3f19eea72631d93c372ab08cac8ffc Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 13 Oct 2023 09:26:36 +0200 Subject: [PATCH 20/61] Cleanup --- .../sdk/internal/di/WorkManagerProvider.kt | 4 ++-- .../session/room/send/DefaultSendService.kt | 1 - .../timeline/TimelineSendEventWorkCommon.kt | 1 - .../session/sync/DefaultSyncService.kt | 1 - .../internal/session/sync/job/SyncWorker.kt | 1 - .../matrix/android/sdk/test/fakes/FakeWork.kt | 20 ------------------- 6 files changed, 2 insertions(+), 26 deletions(-) delete mode 100644 matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeWork.kt diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/WorkManagerProvider.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/WorkManagerProvider.kt index 1728fe9f69..fe021e76dd 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/WorkManagerProvider.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/WorkManagerProvider.kt @@ -107,10 +107,10 @@ internal class WorkManagerProvider @Inject constructor( fun getWorkConstraints( workManagerConfig: WorkManagerConfig, ): Constraints { - val widthNetworkConstraint = workManagerConfig.withNetworkConstraint() + val withNetworkConstraint = workManagerConfig.withNetworkConstraint() return Constraints.Builder() .apply { - if (widthNetworkConstraint) { + if (withNetworkConstraint) { setRequiredNetworkType(NetworkType.CONNECTED) } else { Timber.w("Network constraint is disabled") diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt index 4b2610c033..0c15573aaa 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt @@ -53,7 +53,6 @@ import org.matrix.android.sdk.internal.crypto.store.IMXCommonCryptoStore import org.matrix.android.sdk.internal.di.SessionId import org.matrix.android.sdk.internal.di.WorkManagerProvider import org.matrix.android.sdk.internal.session.content.UploadContentWorker -import org.matrix.android.sdk.internal.session.homeserver.HomeServerCapabilitiesDataSource import org.matrix.android.sdk.internal.session.room.send.queue.EventSenderProcessor import org.matrix.android.sdk.internal.session.workmanager.WorkManagerConfig import org.matrix.android.sdk.internal.task.TaskExecutor diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineSendEventWorkCommon.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineSendEventWorkCommon.kt index ec52cfb2e2..02c541c83d 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineSendEventWorkCommon.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineSendEventWorkCommon.kt @@ -22,7 +22,6 @@ import androidx.work.ListenableWorker import androidx.work.OneTimeWorkRequest import org.matrix.android.sdk.api.util.Cancelable import org.matrix.android.sdk.internal.di.WorkManagerProvider -import org.matrix.android.sdk.internal.session.homeserver.HomeServerCapabilitiesDataSource import org.matrix.android.sdk.internal.session.workmanager.WorkManagerConfig import org.matrix.android.sdk.internal.util.CancelableWork import org.matrix.android.sdk.internal.worker.startChain diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/DefaultSyncService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/DefaultSyncService.kt index 280b8e6c21..bca3f55e2f 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/DefaultSyncService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/DefaultSyncService.kt @@ -20,7 +20,6 @@ import org.matrix.android.sdk.api.session.sync.SyncService import org.matrix.android.sdk.internal.di.SessionId import org.matrix.android.sdk.internal.di.WorkManagerProvider import org.matrix.android.sdk.internal.session.SessionState -import org.matrix.android.sdk.internal.session.homeserver.HomeServerCapabilitiesDataSource import org.matrix.android.sdk.internal.session.sync.job.SyncThread import org.matrix.android.sdk.internal.session.sync.job.SyncWorker import org.matrix.android.sdk.internal.session.workmanager.WorkManagerConfig diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/job/SyncWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/job/SyncWorker.kt index 7a6b929164..abee366730 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/job/SyncWorker.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/job/SyncWorker.kt @@ -25,7 +25,6 @@ import org.matrix.android.sdk.api.failure.isTokenError import org.matrix.android.sdk.internal.SessionManager import org.matrix.android.sdk.internal.di.WorkManagerProvider import org.matrix.android.sdk.internal.session.SessionComponent -import org.matrix.android.sdk.internal.session.homeserver.HomeServerCapabilitiesDataSource import org.matrix.android.sdk.internal.session.sync.SyncPresence import org.matrix.android.sdk.internal.session.sync.SyncTask import org.matrix.android.sdk.internal.session.workmanager.WorkManagerConfig diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeWork.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeWork.kt deleted file mode 100644 index 0d1c478663..0000000000 --- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeWork.kt +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Copyright (c) 2023 New Vector Ltd - * - * 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. - */ - -package org.matrix.android.sdk.test.fakes - -class FakeWork { -} From 64a7de53261ad6ba6c0bdee6c8c7346aebc3b413 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 13 Oct 2023 09:37:58 +0200 Subject: [PATCH 21/61] Ensure Boolean `io.element.disable_network_constraint` is explicitly set in the .well-known file. --- .../sdk/api/session/homeserver/HomeServerCapabilities.kt | 2 +- .../internal/database/migration/MigrateSessionTo054.kt | 1 + .../database/model/HomeServerCapabilitiesEntity.kt | 2 +- .../session/workmanager/DefaultWorkManagerConfig.kt | 9 +++++++-- 4 files changed, 10 insertions(+), 4 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/homeserver/HomeServerCapabilities.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/homeserver/HomeServerCapabilities.kt index a2ea5c4133..ecd03288fc 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/homeserver/HomeServerCapabilities.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/homeserver/HomeServerCapabilities.kt @@ -94,7 +94,7 @@ data class HomeServerCapabilities( /** * If set to true, the SDK will not use the network constraint when configuring Worker for the WorkManager, provided in Wellknown. */ - val disableNetworkConstraint: Boolean = false, + val disableNetworkConstraint: Boolean? = null, ) { enum class RoomCapabilitySupport { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo054.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo054.kt index 0b58c8093d..19f65153cb 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo054.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo054.kt @@ -25,6 +25,7 @@ internal class MigrateSessionTo054(realm: DynamicRealm) : RealmMigrator(realm, 5 override fun doMigrate(realm: DynamicRealm) { realm.schema.get("HomeServerCapabilitiesEntity") ?.addField(HomeServerCapabilitiesEntityFields.DISABLE_NETWORK_CONSTRAINT, Boolean::class.java) + ?.setNullable(HomeServerCapabilitiesEntityFields.DISABLE_NETWORK_CONSTRAINT, true) ?.forceRefreshOfHomeServerCapabilities() } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/HomeServerCapabilitiesEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/HomeServerCapabilitiesEntity.kt index a00dc29087..3891948418 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/HomeServerCapabilitiesEntity.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/HomeServerCapabilitiesEntity.kt @@ -37,7 +37,7 @@ internal open class HomeServerCapabilitiesEntity( var canRedactEventWithRelations: Boolean = false, var externalAccountManagementUrl: String? = null, var authenticationIssuer: String? = null, - var disableNetworkConstraint: Boolean = false, + var disableNetworkConstraint: Boolean? = null, ) : RealmObject() { companion object diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/workmanager/DefaultWorkManagerConfig.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/workmanager/DefaultWorkManagerConfig.kt index aa3fd718cd..159aefc6fd 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/workmanager/DefaultWorkManagerConfig.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/workmanager/DefaultWorkManagerConfig.kt @@ -17,19 +17,24 @@ package org.matrix.android.sdk.internal.session.workmanager import org.matrix.android.sdk.api.auth.data.Credentials -import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.internal.session.homeserver.HomeServerCapabilitiesDataSource import javax.inject.Inject +@Suppress("RedundantIf") internal class DefaultWorkManagerConfig @Inject constructor( private val credentials: Credentials, private val homeServerCapabilitiesDataSource: HomeServerCapabilitiesDataSource, ) : WorkManagerConfig { override fun withNetworkConstraint(): Boolean { return if (credentials.discoveryInformation?.disableNetworkConstraint == true) { + // Boolean `io.element.disable_network_constraint` explicitly set to `true` in the login response + false + } else if (homeServerCapabilitiesDataSource.getHomeServerCapabilities()?.disableNetworkConstraint == true) { + // Boolean `io.element.disable_network_constraint` explicitly set to `true` in the .well-known file false } else { - homeServerCapabilitiesDataSource.getHomeServerCapabilities()?.disableNetworkConstraint.orFalse().not() + // Default, use the Network constraint + true } } } From 8d95eb7b16375dab3af9dad53adf1c93d59e6c1c Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 13 Oct 2023 15:13:48 +0200 Subject: [PATCH 22/61] `disableNetworkConstraint` is now nullable, so do not default the Boolean to false. --- .../session/homeserver/GetHomeServerCapabilitiesTask.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetHomeServerCapabilitiesTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetHomeServerCapabilitiesTask.kt index 05997aeadd..f007f22366 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetHomeServerCapabilitiesTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetHomeServerCapabilitiesTask.kt @@ -20,7 +20,6 @@ import com.zhuinden.monarchy.Monarchy import org.matrix.android.sdk.api.MatrixPatterns.getServerName import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig import org.matrix.android.sdk.api.auth.wellknown.WellknownResult -import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.extensions.orTrue import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilities import org.matrix.android.sdk.internal.auth.version.Versions @@ -168,7 +167,7 @@ internal class DefaultGetHomeServerCapabilitiesTask @Inject constructor( } homeServerCapabilitiesEntity.authenticationIssuer = getWellknownResult.wellKnown.unstableDelegatedAuthConfig?.issuer homeServerCapabilitiesEntity.externalAccountManagementUrl = getWellknownResult.wellKnown.unstableDelegatedAuthConfig?.accountManagementUrl - homeServerCapabilitiesEntity.disableNetworkConstraint = getWellknownResult.wellKnown.disableNetworkConstraint.orFalse() + homeServerCapabilitiesEntity.disableNetworkConstraint = getWellknownResult.wellKnown.disableNetworkConstraint } homeServerCapabilitiesEntity.canLoginWithQrCode = canLoginWithQrCode(getCapabilitiesResult, getVersionResult) From 3aa5f34ee7f9c5179b1bf3bf5cd35bb956690c12 Mon Sep 17 00:00:00 2001 From: Giom Foret Date: Fri, 13 Oct 2023 15:32:47 +0200 Subject: [PATCH 23/61] Update the NetworkConstraint handling in WorkManager config --- .../session/workmanager/DefaultWorkManagerConfig.kt | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/workmanager/DefaultWorkManagerConfig.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/workmanager/DefaultWorkManagerConfig.kt index 159aefc6fd..aa0b7b48a0 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/workmanager/DefaultWorkManagerConfig.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/workmanager/DefaultWorkManagerConfig.kt @@ -26,13 +26,16 @@ internal class DefaultWorkManagerConfig @Inject constructor( private val homeServerCapabilitiesDataSource: HomeServerCapabilitiesDataSource, ) : WorkManagerConfig { override fun withNetworkConstraint(): Boolean { - return if (credentials.discoveryInformation?.disableNetworkConstraint == true) { + val disableNetworkConstraint = homeServerCapabilitiesDataSource.getHomeServerCapabilities()?.disableNetworkConstraint + return if (disableNetworkConstraint != null) { + // Boolean `io.element.disable_network_constraint` explicitly set in the .well-known file + disableNetworkConstraint.not() + } + else if (credentials.discoveryInformation?.disableNetworkConstraint == true) { // Boolean `io.element.disable_network_constraint` explicitly set to `true` in the login response false - } else if (homeServerCapabilitiesDataSource.getHomeServerCapabilities()?.disableNetworkConstraint == true) { - // Boolean `io.element.disable_network_constraint` explicitly set to `true` in the .well-known file - false - } else { + } + else { // Default, use the Network constraint true } From 57d224e8ba5649999ac0eb4407c01af4dbd5f323 Mon Sep 17 00:00:00 2001 From: Giom Foret Date: Fri, 13 Oct 2023 16:39:05 +0200 Subject: [PATCH 24/61] fix Unexpected newlines --- .../session/workmanager/DefaultWorkManagerConfig.kt | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/workmanager/DefaultWorkManagerConfig.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/workmanager/DefaultWorkManagerConfig.kt index aa0b7b48a0..b37597d1c5 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/workmanager/DefaultWorkManagerConfig.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/workmanager/DefaultWorkManagerConfig.kt @@ -30,12 +30,10 @@ internal class DefaultWorkManagerConfig @Inject constructor( return if (disableNetworkConstraint != null) { // Boolean `io.element.disable_network_constraint` explicitly set in the .well-known file disableNetworkConstraint.not() - } - else if (credentials.discoveryInformation?.disableNetworkConstraint == true) { + } else if (credentials.discoveryInformation?.disableNetworkConstraint == true) { // Boolean `io.element.disable_network_constraint` explicitly set to `true` in the login response false - } - else { + } else { // Default, use the Network constraint true } From fd07835e45481c2cd94b578953d8a5b53934e3f4 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 13 Oct 2023 22:01:46 +0200 Subject: [PATCH 25/61] Fix IDE warning --- .../internal/session/workmanager/DefaultWorkManagerConfig.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/workmanager/DefaultWorkManagerConfig.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/workmanager/DefaultWorkManagerConfig.kt index b37597d1c5..44af0f7f7b 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/workmanager/DefaultWorkManagerConfig.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/workmanager/DefaultWorkManagerConfig.kt @@ -20,7 +20,7 @@ import org.matrix.android.sdk.api.auth.data.Credentials import org.matrix.android.sdk.internal.session.homeserver.HomeServerCapabilitiesDataSource import javax.inject.Inject -@Suppress("RedundantIf") +@Suppress("RedundantIf", "IfThenToElvis") internal class DefaultWorkManagerConfig @Inject constructor( private val credentials: Credentials, private val homeServerCapabilitiesDataSource: HomeServerCapabilitiesDataSource, From 17e9bd200bc1bb6de47e0ab30e3ab6cc39e8174a Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 13 Oct 2023 22:02:52 +0200 Subject: [PATCH 26/61] Use Matrix.org copyright. --- .../internal/session/workmanager/DefaultWorkManagerConfig.kt | 2 +- .../sdk/internal/session/workmanager/WorkManagerConfig.kt | 2 +- .../org/matrix/android/sdk/test/fakes/FakeWorkManagerConfig.kt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/workmanager/DefaultWorkManagerConfig.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/workmanager/DefaultWorkManagerConfig.kt index 44af0f7f7b..804eaeb05c 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/workmanager/DefaultWorkManagerConfig.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/workmanager/DefaultWorkManagerConfig.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 New Vector Ltd + * Copyright (c) 2023 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. diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/workmanager/WorkManagerConfig.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/workmanager/WorkManagerConfig.kt index 6e9f2459ad..05523a6cb1 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/workmanager/WorkManagerConfig.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/workmanager/WorkManagerConfig.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 New Vector Ltd + * Copyright (c) 2023 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. diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeWorkManagerConfig.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeWorkManagerConfig.kt index 457bf29db0..e8b47bc408 100644 --- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeWorkManagerConfig.kt +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeWorkManagerConfig.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 New Vector Ltd + * Copyright (c) 2023 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. From 95070d36648dfba87e456cd037fceaeb94024bb1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 11 Aug 2023 23:45:43 +0000 Subject: [PATCH 27/61] Bump io.element.android:wysiwyg from 2.2.2 to 2.5.0 Bumps [io.element.android:wysiwyg](https://github.com/matrix-org/matrix-wysiwyg) from 2.2.2 to 2.5.0. - [Changelog](https://github.com/matrix-org/matrix-rich-text-editor/blob/main/CHANGELOG.md) - [Commits](https://github.com/matrix-org/matrix-wysiwyg/compare/2.2.2...2.5.0) --- updated-dependencies: - dependency-name: io.element.android:wysiwyg dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- dependencies.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dependencies.gradle b/dependencies.gradle index 355f5de661..b7a2113b06 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -101,7 +101,7 @@ ext.libs = [ ], element : [ 'opusencoder' : "io.element.android:opusencoder:1.1.0", - 'wysiwyg' : "io.element.android:wysiwyg:2.2.2" + 'wysiwyg' : "io.element.android:wysiwyg:2.5.0" ], squareup : [ 'moshi' : "com.squareup.moshi:moshi:$moshi", From 6112082d072e8e647b8e2627ad9b98233ffb84bc Mon Sep 17 00:00:00 2001 From: jonnyandrew Date: Fri, 18 Aug 2023 10:52:03 +0100 Subject: [PATCH 28/61] Update to new mentions API --- .../detail/composer/RichTextComposerLayout.kt | 16 ++++------ .../composer/mentions/PillDisplayHandler.kt | 27 ++++++---------- .../mentions/PillDisplayHandlerTest.kt | 31 +++++++------------ 3 files changed, 27 insertions(+), 47 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/RichTextComposerLayout.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/RichTextComposerLayout.kt index 48163a43bf..1b999b65c8 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/RichTextComposerLayout.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/RichTextComposerLayout.kt @@ -51,8 +51,7 @@ import im.vector.app.databinding.ViewRichTextMenuButtonBinding import im.vector.app.features.home.room.detail.composer.images.UriContentListener import im.vector.app.features.home.room.detail.composer.mentions.PillDisplayHandler import io.element.android.wysiwyg.EditorEditText -import io.element.android.wysiwyg.display.KeywordDisplayHandler -import io.element.android.wysiwyg.display.LinkDisplayHandler +import io.element.android.wysiwyg.display.MentionDisplayHandler import io.element.android.wysiwyg.display.TextDisplay import io.element.android.wysiwyg.utils.RustErrorCollector import io.element.android.wysiwyg.view.models.InlineFormat @@ -238,15 +237,12 @@ internal class RichTextComposerLayout @JvmOverloads constructor( views.composerEditTextOuterBorder.background = borderShapeDrawable setupRichTextMenu() - views.richTextComposerEditText.linkDisplayHandler = LinkDisplayHandler { text, url -> - pillDisplayHandler?.resolveLinkDisplay(text, url) ?: TextDisplay.Plain - } - views.richTextComposerEditText.keywordDisplayHandler = object : KeywordDisplayHandler { - override val keywords: List - get() = pillDisplayHandler?.keywords.orEmpty() + views.richTextComposerEditText.mentionDisplayHandler = object : MentionDisplayHandler { + override fun resolveMentionDisplay(text: String, url: String): TextDisplay = + pillDisplayHandler?.resolveMentionDisplay(text, url) ?: TextDisplay.Plain - override fun resolveKeywordDisplay(text: String): TextDisplay = - pillDisplayHandler?.resolveKeywordDisplay(text) ?: TextDisplay.Plain + override fun resolveAtRoomMentionDisplay(): TextDisplay = + pillDisplayHandler?.resolveAtRoomMentionDisplay() ?: TextDisplay.Plain } updateTextFieldBorder(isFullScreen) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/mentions/PillDisplayHandler.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/mentions/PillDisplayHandler.kt index c2b71ea15b..e5e6aa347d 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/mentions/PillDisplayHandler.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/mentions/PillDisplayHandler.kt @@ -17,8 +17,7 @@ package im.vector.app.features.home.room.detail.composer.mentions import android.text.style.ReplacementSpan -import io.element.android.wysiwyg.display.KeywordDisplayHandler -import io.element.android.wysiwyg.display.LinkDisplayHandler +import io.element.android.wysiwyg.display.MentionDisplayHandler import io.element.android.wysiwyg.display.TextDisplay import org.matrix.android.sdk.api.session.permalinks.PermalinkData import org.matrix.android.sdk.api.session.permalinks.PermalinkParser @@ -30,16 +29,15 @@ import org.matrix.android.sdk.api.util.toMatrixItem import org.matrix.android.sdk.api.util.toRoomAliasMatrixItem /** - * A rich text editor [LinkDisplayHandler] and [KeywordDisplayHandler] - * that helps with replacing user and room links with pills. + * A rich text editor [MentionDisplayHandler] that helps with replacing user and room links with pills. */ internal class PillDisplayHandler( private val roomId: String, private val getRoom: (roomId: String) -> RoomSummary?, private val getMember: (userId: String) -> RoomMemberSummary?, private val replacementSpanFactory: (matrixItem: MatrixItem) -> ReplacementSpan, -) : LinkDisplayHandler, KeywordDisplayHandler { - override fun resolveLinkDisplay(text: String, url: String): TextDisplay { +) : MentionDisplayHandler { + override fun resolveMentionDisplay(text: String, url: String): TextDisplay { val matrixItem = when (val permalink = PermalinkParser.parse(url)) { is PermalinkData.UserLink -> { val userId = permalink.userId @@ -65,16 +63,9 @@ internal class PillDisplayHandler( return TextDisplay.Custom(customSpan = replacement) } - override val keywords: List - get() = listOf(MatrixItem.NOTIFY_EVERYONE) - - override fun resolveKeywordDisplay(text: String): TextDisplay = - when (text) { - MatrixItem.NOTIFY_EVERYONE -> { - val matrixItem = getRoom(roomId)?.toEveryoneInRoomMatrixItem() - ?: MatrixItem.EveryoneInRoomItem(roomId) - TextDisplay.Custom(replacementSpanFactory.invoke(matrixItem)) - } - else -> TextDisplay.Plain - } + override fun resolveAtRoomMentionDisplay(): TextDisplay { + val matrixItem = getRoom(roomId)?.toEveryoneInRoomMatrixItem() + ?: MatrixItem.EveryoneInRoomItem(roomId) + return TextDisplay.Custom(replacementSpanFactory.invoke(matrixItem)) + } } diff --git a/vector/src/test/java/im/vector/app/features/home/room/detail/composer/mentions/PillDisplayHandlerTest.kt b/vector/src/test/java/im/vector/app/features/home/room/detail/composer/mentions/PillDisplayHandlerTest.kt index 57c7aee420..be5a6a93ee 100644 --- a/vector/src/test/java/im/vector/app/features/home/room/detail/composer/mentions/PillDisplayHandlerTest.kt +++ b/vector/src/test/java/im/vector/app/features/home/room/detail/composer/mentions/PillDisplayHandlerTest.kt @@ -80,7 +80,7 @@ internal class PillDisplayHandlerTest { fun `when resolve non-matrix link, then it returns plain text`() { val subject = createSubject() - val result = subject.resolveLinkDisplay("text", NON_MATRIX_URL) + val result = subject.resolveMentionDisplay("text", NON_MATRIX_URL) assertEquals(TextDisplay.Plain, result) } @@ -89,7 +89,7 @@ internal class PillDisplayHandlerTest { fun `when resolve unknown user link, then it returns generic custom pill`() { val subject = createSubject() - val matrixItem = subject.resolveLinkDisplay("text", UNKNOWN_MATRIX_USER_URL) + val matrixItem = subject.resolveMentionDisplay("text", UNKNOWN_MATRIX_USER_URL) .getMatrixItem() assertEquals(MatrixItem.UserItem(UNKNOWN_MATRIX_USER_ID, UNKNOWN_MATRIX_USER_ID, null), matrixItem) @@ -99,7 +99,7 @@ internal class PillDisplayHandlerTest { fun `when resolve known user link, then it returns named custom pill`() { val subject = createSubject() - val matrixItem = subject.resolveLinkDisplay("text", KNOWN_MATRIX_USER_URL) + val matrixItem = subject.resolveMentionDisplay("text", KNOWN_MATRIX_USER_URL) .getMatrixItem() assertEquals(MatrixItem.UserItem(KNOWN_MATRIX_USER_ID, KNOWN_MATRIX_USER_NAME, KNOWN_MATRIX_USER_AVATAR), matrixItem) @@ -109,7 +109,7 @@ internal class PillDisplayHandlerTest { fun `when resolve unknown room link, then it returns generic custom pill`() { val subject = createSubject() - val matrixItem = subject.resolveLinkDisplay("text", UNKNOWN_MATRIX_ROOM_URL) + val matrixItem = subject.resolveMentionDisplay("text", UNKNOWN_MATRIX_ROOM_URL) .getMatrixItem() assertEquals(MatrixItem.RoomItem(UNKNOWN_MATRIX_ROOM_ID, UNKNOWN_MATRIX_ROOM_ID, null), matrixItem) @@ -119,7 +119,7 @@ internal class PillDisplayHandlerTest { fun `when resolve known room link, then it returns named custom pill`() { val subject = createSubject() - val matrixItem = subject.resolveLinkDisplay("text", KNOWN_MATRIX_ROOM_URL) + val matrixItem = subject.resolveMentionDisplay("text", KNOWN_MATRIX_ROOM_URL) .getMatrixItem() assertEquals(MatrixItem.RoomItem(KNOWN_MATRIX_ROOM_ID, KNOWN_MATRIX_ROOM_NAME, KNOWN_MATRIX_ROOM_AVATAR), matrixItem) @@ -129,7 +129,7 @@ internal class PillDisplayHandlerTest { fun `when resolve @room link, then it returns room notification custom pill`() { val subject = createSubject() - val matrixItem = subject.resolveLinkDisplay("@room", KNOWN_MATRIX_ROOM_URL) + val matrixItem = subject.resolveMentionDisplay("@room", KNOWN_MATRIX_ROOM_URL) .getMatrixItem() assertEquals(MatrixItem.EveryoneInRoomItem(KNOWN_MATRIX_ROOM_ID, NOTIFY_EVERYONE, KNOWN_MATRIX_ROOM_AVATAR, KNOWN_MATRIX_ROOM_NAME), matrixItem) @@ -139,7 +139,7 @@ internal class PillDisplayHandlerTest { fun `when resolve @room keyword, then it returns room notification custom pill`() { val subject = createSubject() - val matrixItem = subject.resolveKeywordDisplay("@room") + val matrixItem = subject.resolveAtRoomMentionDisplay() .getMatrixItem() assertEquals(MatrixItem.EveryoneInRoomItem(ROOM_ID, NOTIFY_EVERYONE, KNOWN_MATRIX_ROOM_AVATAR, KNOWN_MATRIX_ROOM_NAME), matrixItem) @@ -150,24 +150,17 @@ internal class PillDisplayHandlerTest { val subject = createSubject() every { mockGetRoom(ROOM_ID) } returns null - val matrixItem = subject.resolveKeywordDisplay("@room") + val matrixItem = subject.resolveAtRoomMentionDisplay() .getMatrixItem() assertEquals(MatrixItem.EveryoneInRoomItem(ROOM_ID, NOTIFY_EVERYONE, null, null), matrixItem) } - @Test - fun `when get keywords, then it returns @room`() { - val subject = createSubject() - - assertEquals(listOf("@room"), subject.keywords) - } - @Test fun `when resolve known user for custom domain link, then it returns named custom pill`() { val subject = createSubject() - val matrixItem = subject.resolveLinkDisplay("text", CUSTOM_DOMAIN_MATRIX_USER_URL) + val matrixItem = subject.resolveMentionDisplay("text", CUSTOM_DOMAIN_MATRIX_USER_URL) .getMatrixItem() assertEquals(MatrixItem.UserItem(KNOWN_MATRIX_USER_ID, KNOWN_MATRIX_USER_NAME, KNOWN_MATRIX_USER_AVATAR), matrixItem) @@ -177,7 +170,7 @@ internal class PillDisplayHandlerTest { fun `when resolve known room for custom domain link, then it returns named custom pill`() { val subject = createSubject() - val matrixItem = subject.resolveLinkDisplay("text", CUSTOM_DOMAIN_MATRIX_ROOM_URL) + val matrixItem = subject.resolveMentionDisplay("text", CUSTOM_DOMAIN_MATRIX_ROOM_URL) .getMatrixItem() assertEquals(MatrixItem.RoomItem(KNOWN_MATRIX_ROOM_ID, KNOWN_MATRIX_ROOM_NAME, KNOWN_MATRIX_ROOM_AVATAR), matrixItem) @@ -187,13 +180,13 @@ internal class PillDisplayHandlerTest { fun `when resolve known room with alias link, then it returns named custom pill`() { val subject = createSubject() - val matrixItem = subject.resolveLinkDisplay("text", KNOWN_MATRIX_ROOM_ALIAS_URL) + val matrixItem = subject.resolveMentionDisplay("text", KNOWN_MATRIX_ROOM_ALIAS_URL) .getMatrixItem() assertEquals(MatrixItem.RoomAliasItem(KNOWN_MATRIX_ROOM_ALIAS, KNOWN_MATRIX_ROOM_NAME, KNOWN_MATRIX_ROOM_AVATAR), matrixItem) } - private fun TextDisplay.getMatrixItem(): MatrixItem? { + private fun TextDisplay.getMatrixItem(): MatrixItem { val customSpan = this as? TextDisplay.Custom assertNotNull("The URL did not resolve to a custom link display method", customSpan) From 531d9f280259b47d7e8ad1fd2182b746db120b03 Mon Sep 17 00:00:00 2001 From: jonnyandrew Date: Mon, 21 Aug 2023 15:46:42 +0100 Subject: [PATCH 29/61] Update to 2.6.0 --- dependencies.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dependencies.gradle b/dependencies.gradle index b7a2113b06..3d8d79d3a7 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -101,7 +101,7 @@ ext.libs = [ ], element : [ 'opusencoder' : "io.element.android:opusencoder:1.1.0", - 'wysiwyg' : "io.element.android:wysiwyg:2.5.0" + 'wysiwyg' : "io.element.android:wysiwyg:2.6.0" ], squareup : [ 'moshi' : "com.squareup.moshi:moshi:$moshi", From 26e2f4e9677204681a2859e40787d83411c7e2bb Mon Sep 17 00:00:00 2001 From: jonnyandrew Date: Fri, 25 Aug 2023 11:56:43 +0100 Subject: [PATCH 30/61] Fix compilation error --- .../app/features/pin/lockscreen/biometrics/BiometricHelper.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/pin/lockscreen/biometrics/BiometricHelper.kt b/vector/src/main/java/im/vector/app/features/pin/lockscreen/biometrics/BiometricHelper.kt index bf2075d3a8..b10fd26aff 100644 --- a/vector/src/main/java/im/vector/app/features/pin/lockscreen/biometrics/BiometricHelper.kt +++ b/vector/src/main/java/im/vector/app/features/pin/lockscreen/biometrics/BiometricHelper.kt @@ -41,7 +41,7 @@ import im.vector.app.features.pin.lockscreen.ui.fallbackprompt.FallbackBiometric import im.vector.app.features.pin.lockscreen.utils.DevicePromptCheck import im.vector.app.features.pin.lockscreen.utils.hasFlag import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.asCoroutineDispatcher import kotlinx.coroutines.cancel import kotlinx.coroutines.channels.BufferOverflow @@ -155,7 +155,7 @@ class BiometricHelper @AssistedInject constructor( return authenticate(activity) } - @OptIn(ExperimentalCoroutinesApi::class) + @OptIn(DelicateCoroutinesApi::class) private fun authenticateInternal( activity: FragmentActivity, checkSystemKeyExists: Boolean, From 8d8a5d3de234f64678c15fa81d253cf5859bbe7f Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 6 Nov 2023 18:17:14 +0100 Subject: [PATCH 31/61] Upgrade Wysiwyg library to 2.14.1 --- dependencies.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dependencies.gradle b/dependencies.gradle index 3d8d79d3a7..f69da829d6 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -101,7 +101,7 @@ ext.libs = [ ], element : [ 'opusencoder' : "io.element.android:opusencoder:1.1.0", - 'wysiwyg' : "io.element.android:wysiwyg:2.6.0" + 'wysiwyg' : "io.element.android:wysiwyg:2.14.1" ], squareup : [ 'moshi' : "com.squareup.moshi:moshi:$moshi", From 4341cf8c9c7112c9aeebb488b33376da07097907 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 6 Nov 2023 18:29:47 +0100 Subject: [PATCH 32/61] Upgrade Mavericks library to 3.0.7 It fixes crash: java.lang.IllegalStateException: Flow invariant is violated --- dependencies.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dependencies.gradle b/dependencies.gradle index f69da829d6..cc1abb3cf6 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -20,7 +20,7 @@ def lifecycle = "2.5.1" def flowBinding = "1.2.0" def flipper = "0.190.0" def epoxy = "5.0.0" -def mavericks = "3.0.2" +def mavericks = "3.0.7" def glide = "4.15.1" def bigImageViewer = "1.8.1" def jjwt = "0.11.5" From 83084f64811be065a57605765faaab6aa50cf4e3 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 7 Nov 2023 11:09:30 +0100 Subject: [PATCH 33/61] Upgrade lint --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index ea70ad5e51..682c94a916 100644 --- a/gradle.properties +++ b/gradle.properties @@ -42,4 +42,4 @@ signing.element.nightly.keyPassword=Secret # Customise the Lint version to use a more recent version than the one bundled with AGP # https://googlesamples.github.io/android-custom-lint-rules/usage/newer-lint.md.html -android.experimental.lint.version=8.0.0-alpha10 +android.experimental.lint.version=8.3.0-alpha12 From b14338d2c4b46fdfdadd3035e2b694c1a4970142 Mon Sep 17 00:00:00 2001 From: Yoan Pintas Date: Tue, 7 Nov 2023 14:12:27 +0100 Subject: [PATCH 34/61] Remove unused WebRTC dependency (#8658) --- changelog.d/8658.misc | 1 + vector-app/build.gradle | 2 +- vector/build.gradle | 4 ---- 3 files changed, 2 insertions(+), 5 deletions(-) create mode 100644 changelog.d/8658.misc diff --git a/changelog.d/8658.misc b/changelog.d/8658.misc new file mode 100644 index 0000000000..f4cf3d051d --- /dev/null +++ b/changelog.d/8658.misc @@ -0,0 +1 @@ +Remove unused WebRTC dependency diff --git a/vector-app/build.gradle b/vector-app/build.gradle index ce05b92902..808610510b 100644 --- a/vector-app/build.gradle +++ b/vector-app/build.gradle @@ -309,7 +309,7 @@ android { } } - flavorDimensions "store", "crypto" + flavorDimensions = ["store", "crypto"] productFlavors { gplay { diff --git a/vector/build.gradle b/vector/build.gradle index e45d23a11e..f1ec2b0d32 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -243,10 +243,6 @@ dependencies { implementation "androidx.emoji2:emoji2:1.3.0" - // WebRTC - // org.webrtc:google-webrtc is for development purposes only - // implementation 'org.webrtc:google-webrtc:1.0.+' - implementation('com.facebook.react:react-native-webrtc:111.0.0-jitsi-13672566@aar') // Jitsi api('org.jitsi.react:jitsi-meet-sdk:8.1.1') { exclude group: 'com.google.firebase' From 2c75f410728f09ce7294b9ae113449304a8903ff Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 8 Nov 2023 17:49:04 +0100 Subject: [PATCH 35/61] Fix lint false positive --- .../pin/lockscreen/biometrics/BiometricHelper.kt | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/pin/lockscreen/biometrics/BiometricHelper.kt b/vector/src/main/java/im/vector/app/features/pin/lockscreen/biometrics/BiometricHelper.kt index b10fd26aff..bcfd651ce2 100644 --- a/vector/src/main/java/im/vector/app/features/pin/lockscreen/biometrics/BiometricHelper.kt +++ b/vector/src/main/java/im/vector/app/features/pin/lockscreen/biometrics/BiometricHelper.kt @@ -157,9 +157,9 @@ class BiometricHelper @AssistedInject constructor( @OptIn(DelicateCoroutinesApi::class) private fun authenticateInternal( - activity: FragmentActivity, - checkSystemKeyExists: Boolean, - cryptoObject: BiometricPrompt.CryptoObject, + activity: FragmentActivity, + checkSystemKeyExists: Boolean, + cryptoObject: BiometricPrompt.CryptoObject, ): Flow { if (checkSystemKeyExists && !isSystemAuthEnabledAndValid) return flowOf(false) @@ -195,9 +195,9 @@ class BiometricHelper @AssistedInject constructor( @VisibleForTesting(otherwise = PRIVATE) internal fun authenticateWithPromptInternal( - activity: FragmentActivity, - cryptoObject: BiometricPrompt.CryptoObject, - channel: Channel, + activity: FragmentActivity, + cryptoObject: BiometricPrompt.CryptoObject, + channel: Channel, ): BiometricPrompt { val executor = ContextCompat.getMainExecutor(context) val callback = createSuspendingAuthCallback(channel, executor.asCoroutineDispatcher()) @@ -314,9 +314,9 @@ class BiometricHelper @AssistedInject constructor( fallbackFragment.onDismiss = { cancelPrompt() } fallbackFragment.authenticationFlow = authenticationFLow - activity.supportFragmentManager.beginTransaction() + val transaction = activity.supportFragmentManager.beginTransaction() .runOnCommit { scope.launch { showPrompt() } } - .apply { fallbackFragment.show(this, FALLBACK_BIOMETRIC_FRAGMENT_TAG) } + fallbackFragment.show(transaction, FALLBACK_BIOMETRIC_FRAGMENT_TAG) } else { scope.launch { showPrompt() } } From dd6410794c715db42a1165fed92ecfddb91077b6 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 8 Nov 2023 17:51:03 +0100 Subject: [PATCH 36/61] Suppress lint warning. MenuBuilder is restricted. --- .../features/settings/devices/v2/list/SessionsListHeaderView.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/SessionsListHeaderView.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/SessionsListHeaderView.kt index f74d88790c..42d9f01595 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/SessionsListHeaderView.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/SessionsListHeaderView.kt @@ -97,6 +97,7 @@ class SessionsListHeaderView @JvmOverloads constructor( } } + @Suppress("RestrictedApi") private fun setMenu(typedArray: TypedArray) { val menuResId = typedArray.getResourceId(R.styleable.SessionsListHeaderView_sessionsListHeaderMenu, -1) if (menuResId == -1) { From 4e53d8462f8a7137df289c4f880fbf6167601aed Mon Sep 17 00:00:00 2001 From: Valere Date: Fri, 10 Nov 2023 16:33:46 +0100 Subject: [PATCH 37/61] use error instead of require --- .../android/sdk/internal/crypto/RustCrossSigningService.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/RustCrossSigningService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/RustCrossSigningService.kt index c4fd06bcd0..e2def5af8a 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/RustCrossSigningService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/RustCrossSigningService.kt @@ -174,10 +174,10 @@ internal class RustCrossSigningService @Inject constructor( if (verified) { return } else { - require(false) { "This device [$deviceId] is not known, or not yours" } + error("This device [$deviceId] is not known, or not yours") } } else { - require(false) { "This device [$deviceId] is not known" } + error("This device [$deviceId] is not known") } } From 58a44ac66833c70786eb8920558f223df39f9885 Mon Sep 17 00:00:00 2001 From: Valere Date: Mon, 13 Nov 2023 14:00:30 +0100 Subject: [PATCH 38/61] fix test --- .../features/home/RoomsListViewModelTest.kt | 30 +++++++++++++++++-- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/vector/src/test/java/im/vector/app/features/home/RoomsListViewModelTest.kt b/vector/src/test/java/im/vector/app/features/home/RoomsListViewModelTest.kt index a601505d6c..a90f8cd211 100644 --- a/vector/src/test/java/im/vector/app/features/home/RoomsListViewModelTest.kt +++ b/vector/src/test/java/im/vector/app/features/home/RoomsListViewModelTest.kt @@ -18,6 +18,9 @@ package im.vector.app.features.home import android.widget.ImageView import androidx.arch.core.executor.testing.InstantTaskExecutorRule +import androidx.lifecycle.LiveData +import androidx.lifecycle.liveData +import androidx.paging.PagedList import com.airbnb.mvrx.test.MavericksTestRule import im.vector.app.R import im.vector.app.core.platform.StateView @@ -35,6 +38,7 @@ import im.vector.app.test.fakes.FakeStringProvider import im.vector.app.test.fixtures.RoomSummaryFixture.aRoomSummary import im.vector.app.test.test import io.mockk.every +import io.mockk.mockk import io.mockk.mockkStatic import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.runTest @@ -43,7 +47,12 @@ import org.junit.Rule import org.junit.Test import org.matrix.android.sdk.api.query.SpaceFilter import org.matrix.android.sdk.api.session.getUserOrDefault +import org.matrix.android.sdk.api.session.room.ResultBoundaries +import org.matrix.android.sdk.api.session.room.RoomSortOrder +import org.matrix.android.sdk.api.session.room.RoomSummaryQueryParams +import org.matrix.android.sdk.api.session.room.UpdatableLivePageResult import org.matrix.android.sdk.api.session.room.model.Membership +import org.matrix.android.sdk.api.session.room.model.RoomSummary import org.matrix.android.sdk.api.util.Optional import org.matrix.android.sdk.api.util.toMatrixItem import org.matrix.android.sdk.flow.FlowSession @@ -81,16 +90,31 @@ class RoomsListViewModelTest { val roomC = aRoomSummary("room_c") val allRooms = listOf(roomA, roomB, roomC) + val mockPagedList = mockk>().apply { + every { get(any()) } answers { + allRooms[firstArg()] + } + + every { loadedCount } returns allRooms.size + } + every { - fakeFLowSession.liveRoomSummaries( + fakeSession.fakeRoomService.getFilteredPagedRoomSummariesLive( match { it.roomCategoryFilter == null && it.roomTagQueryFilter == null && it.memberships == listOf(Membership.JOIN) && it.spaceFilter is SpaceFilter.NoFilter - }, any() + }, any(), any() ) - } returns flowOf(allRooms) + } returns object : UpdatableLivePageResult { + override val livePagedList: LiveData> + get() = liveData { emit(mockPagedList) } + override val liveBoundaries: LiveData + get() = liveData { emit(ResultBoundaries(true, true, false)) } + override var queryParams = RoomSummaryQueryParams.Builder().build() + override var sortOrder = RoomSortOrder.ACTIVITY + } viewModelWith(initialState) } From d045cedb469d3c3aa763745763d2a96cfe3c09a5 Mon Sep 17 00:00:00 2001 From: Valere Date: Mon, 13 Nov 2023 15:01:19 +0100 Subject: [PATCH 39/61] ignore paparazzi tests --- .../im/vector/app/screenshot/PaparazziExampleScreenshotTest.kt | 2 ++ .../java/im/vector/app/screenshot/RoomItemScreenshotTest.kt | 2 ++ 2 files changed, 4 insertions(+) diff --git a/vector/src/test/java/im/vector/app/screenshot/PaparazziExampleScreenshotTest.kt b/vector/src/test/java/im/vector/app/screenshot/PaparazziExampleScreenshotTest.kt index 58658651cf..6bfee9ec19 100644 --- a/vector/src/test/java/im/vector/app/screenshot/PaparazziExampleScreenshotTest.kt +++ b/vector/src/test/java/im/vector/app/screenshot/PaparazziExampleScreenshotTest.kt @@ -20,9 +20,11 @@ import android.widget.ImageView import android.widget.TextView import androidx.constraintlayout.widget.ConstraintLayout import im.vector.app.R +import org.junit.Ignore import org.junit.Rule import org.junit.Test +@Ignore("CI failing with NPE on paparazzi.inflate") class PaparazziExampleScreenshotTest { @get:Rule diff --git a/vector/src/test/java/im/vector/app/screenshot/RoomItemScreenshotTest.kt b/vector/src/test/java/im/vector/app/screenshot/RoomItemScreenshotTest.kt index d1f4034f43..c73531b07a 100644 --- a/vector/src/test/java/im/vector/app/screenshot/RoomItemScreenshotTest.kt +++ b/vector/src/test/java/im/vector/app/screenshot/RoomItemScreenshotTest.kt @@ -22,9 +22,11 @@ import androidx.constraintlayout.widget.ConstraintLayout import androidx.core.view.isVisible import im.vector.app.R import im.vector.app.features.home.room.list.UnreadCounterBadgeView +import org.junit.Ignore import org.junit.Rule import org.junit.Test +@Ignore("CI failing with NPE on paparazzi.inflate") class RoomItemScreenshotTest { @get:Rule From b61b2b6f16e077315200ae09578366c65fbebfcd Mon Sep 17 00:00:00 2001 From: Valere Date: Mon, 13 Nov 2023 22:23:11 +0100 Subject: [PATCH 40/61] bump crypto sdk to 0.3.16 --- matrix-sdk-android/build.gradle | 2 +- .../sdk/api/session/crypto/CryptoService.kt | 3 ++- .../android/sdk/internal/crypto/OlmMachine.kt | 24 +++++++++---------- .../sdk/internal/crypto/RustCryptoService.kt | 3 ++- .../session/sync/SyncResponseHandler.kt | 3 ++- 5 files changed, 18 insertions(+), 17 deletions(-) diff --git a/matrix-sdk-android/build.gradle b/matrix-sdk-android/build.gradle index e0dae499bb..cab81b0283 100644 --- a/matrix-sdk-android/build.gradle +++ b/matrix-sdk-android/build.gradle @@ -215,7 +215,7 @@ dependencies { implementation libs.google.phonenumber - implementation("org.matrix.rustcomponents:crypto-android:0.3.15") + implementation("org.matrix.rustcomponents:crypto-android:0.3.16") // api project(":library:rustCrypto") testImplementation libs.tests.junit diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/CryptoService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/CryptoService.kt index 31d11f6730..3ed6dd1450 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/CryptoService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/CryptoService.kt @@ -240,7 +240,8 @@ interface CryptoService { toDevice: ToDeviceSyncResponse?, deviceChanges: DeviceListResponse?, keyCounts: DeviceOneTimeKeysCountSyncResponse?, - deviceUnusedFallbackKeyTypes: List?) + deviceUnusedFallbackKeyTypes: List?, + nextBatch: String?) suspend fun onLiveEvent(roomId: String, event: Event, isInitialSync: Boolean, cryptoStoreAggregator: CryptoStoreAggregator?) suspend fun onStateEvent(roomId: String, event: Event, cryptoStoreAggregator: CryptoStoreAggregator?) {} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OlmMachine.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OlmMachine.kt index 998c6ef753..225852a094 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OlmMachine.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OlmMachine.kt @@ -19,7 +19,6 @@ package org.matrix.android.sdk.internal.crypto import androidx.lifecycle.LiveData import androidx.lifecycle.asLiveData import com.squareup.moshi.Moshi -import com.squareup.moshi.Types import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.channelFlow import kotlinx.coroutines.runBlocking @@ -262,6 +261,7 @@ internal class OlmMachine @Inject constructor( deviceChanges: DeviceListResponse?, keyCounts: DeviceOneTimeKeysCountSyncResponse?, deviceUnusedFallbackKeyTypes: List?, + nextBatch: String? ): ToDeviceSyncResponse { val response = withContext(coroutineDispatchers.io) { val counts: MutableMap = mutableMapOf() @@ -281,18 +281,16 @@ internal class OlmMachine @Inject constructor( val events = adapter.toJson(toDevice ?: ToDeviceSyncResponse()) // field pass in the list of unused fallback keys here - val receiveSyncChanges = inner.receiveSyncChanges(events, devices, counts, deviceUnusedFallbackKeyTypes) - - val outAdapter = moshi.adapter>( - Types.newParameterizedType( - List::class.java, - Event::class.java, - String::class.java, - Integer::class.java, - Any::class.java, - ) - ) - outAdapter.fromJson(receiveSyncChanges) ?: emptyList() + val receiveSyncChanges = inner.receiveSyncChanges(events, devices, counts, deviceUnusedFallbackKeyTypes, nextBatch ?: "") + + val outAdapter = moshi.adapter(Event::class.java) + + // we don't need to use `roomKeyInfos` as we are for now we are + // checking the returned to devices to check for room keys. + // XXX Anyhow there is now proper signaling we should soon stop parsing them manually + receiveSyncChanges.toDeviceEvents.map { + outAdapter.fromJson(it) ?: Event() + } } // We may get cross signing keys over a to-device event, update our listeners. diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/RustCryptoService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/RustCryptoService.kt index 558b186cdd..44fd9e6797 100755 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/RustCryptoService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/RustCryptoService.kt @@ -618,9 +618,10 @@ internal class RustCryptoService @Inject constructor( deviceChanges: DeviceListResponse?, keyCounts: DeviceOneTimeKeysCountSyncResponse?, deviceUnusedFallbackKeyTypes: List?, + nextBatch: String?, ) { // Decrypt and handle our to-device events - val toDeviceEvents = this.olmMachine.receiveSyncChanges(toDevice, deviceChanges, keyCounts, deviceUnusedFallbackKeyTypes) + val toDeviceEvents = this.olmMachine.receiveSyncChanges(toDevice, deviceChanges, keyCounts, deviceUnusedFallbackKeyTypes, nextBatch) // Notify the our listeners about room keys so decryption is retried. toDeviceEvents.events.orEmpty().forEach { event -> diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncResponseHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncResponseHandler.kt index 3d0c816cb0..8331786072 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncResponseHandler.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncResponseHandler.kt @@ -184,7 +184,8 @@ internal class SyncResponseHandler @Inject constructor( syncResponse.toDevice, syncResponse.deviceLists, syncResponse.deviceOneTimeKeysCount, - syncResponse.deviceUnusedFallbackKeyTypes + syncResponse.deviceUnusedFallbackKeyTypes, + syncResponse.nextBatch ) }.also { Timber.v("Finish handling toDevice in $it ms") From f37d918ce6dd08fc6dbaac1a54f93e29c59323d3 Mon Sep 17 00:00:00 2001 From: Valere Date: Tue, 14 Nov 2023 11:24:05 +0100 Subject: [PATCH 41/61] fix outdated documentation --- .../java/org/matrix/android/sdk/internal/crypto/OlmMachine.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OlmMachine.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OlmMachine.kt index 225852a094..639c8f55d2 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OlmMachine.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OlmMachine.kt @@ -253,6 +253,8 @@ internal class OlmMachine @Inject constructor( * * @param deviceUnusedFallbackKeyTypes The key algorithms for which the server has an unused fallback key for the device. * + * @param nextBatch The batch token to pass in the next sync request. + * * @return The handled events, decrypted if needed (secrets are zeroised). */ @Throws(CryptoStoreException::class) From 36ce42e36ef3e4470c48b9fef963936d9b96dbae Mon Sep 17 00:00:00 2001 From: Valere Date: Tue, 14 Nov 2023 11:33:26 +0100 Subject: [PATCH 42/61] update change log --- changelog.d/8679.misc | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/8679.misc diff --git a/changelog.d/8679.misc b/changelog.d/8679.misc new file mode 100644 index 0000000000..d913671219 --- /dev/null +++ b/changelog.d/8679.misc @@ -0,0 +1 @@ +Bump crypto sdk bindings to v0.3.16 From d3391076b52020e1c24dc17e76dc6c526da8a3d4 Mon Sep 17 00:00:00 2001 From: Valere Date: Tue, 14 Nov 2023 12:09:05 +0100 Subject: [PATCH 43/61] fix comment --- .../java/org/matrix/android/sdk/internal/crypto/OlmMachine.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OlmMachine.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OlmMachine.kt index 639c8f55d2..f90ae4a345 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OlmMachine.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OlmMachine.kt @@ -287,7 +287,7 @@ internal class OlmMachine @Inject constructor( val outAdapter = moshi.adapter(Event::class.java) - // we don't need to use `roomKeyInfos` as we are for now we are + // we don't need to use `roomKeyInfos` as for now we are manually // checking the returned to devices to check for room keys. // XXX Anyhow there is now proper signaling we should soon stop parsing them manually receiveSyncChanges.toDeviceEvents.map { From 3179dc1400caf3ea5b26bf8f67f8a6a73861665d Mon Sep 17 00:00:00 2001 From: yostyle Date: Mon, 23 Oct 2023 17:28:08 +0200 Subject: [PATCH 44/61] Update regex for email address --- .../org/matrix/android/sdk/api/extensions/Strings.kt | 10 ++++++++++ .../sdk/internal/auth/login/DefaultLoginWizard.kt | 4 ++-- .../im/vector/app/core/extensions/BasicExtensions.kt | 3 ++- 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/extensions/Strings.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/extensions/Strings.kt index 9f979098f8..ec5a8bc644 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/extensions/Strings.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/extensions/Strings.kt @@ -16,6 +16,11 @@ package org.matrix.android.sdk.api.extensions +import java.util.regex.Pattern + +const val emailPattern = "^[a-zA-Z0-9_!#\$%&'*+/=?`{|}~^-]+(?:\\.[a-zA-Z0-9_!#\$%&'*+/=?`{|}~^-]+)*@[a-zA-Z0-9-]+(?:\\.[a-zA-Z0-9-]+)*\$" +val emailAddress: Pattern = Pattern.compile(emailPattern) + fun CharSequence.ensurePrefix(prefix: CharSequence): CharSequence { return when { startsWith(prefix) -> this @@ -23,6 +28,11 @@ fun CharSequence.ensurePrefix(prefix: CharSequence): CharSequence { } } +/** + * Check if a CharSequence is an email. + */ +fun CharSequence.isEmail() = emailAddress.matcher(this).matches() + /** * Append a new line and then the provided string. */ diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/login/DefaultLoginWizard.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/login/DefaultLoginWizard.kt index 0a8c58de16..86341729ca 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/login/DefaultLoginWizard.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/login/DefaultLoginWizard.kt @@ -16,11 +16,11 @@ package org.matrix.android.sdk.internal.auth.login -import android.util.Patterns import org.matrix.android.sdk.api.auth.LoginType import org.matrix.android.sdk.api.auth.login.LoginProfileInfo import org.matrix.android.sdk.api.auth.login.LoginWizard import org.matrix.android.sdk.api.auth.registration.RegisterThreePid +import org.matrix.android.sdk.api.extensions.isEmail import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.util.JsonDict import org.matrix.android.sdk.internal.auth.AuthAPI @@ -59,7 +59,7 @@ internal class DefaultLoginWizard( initialDeviceName: String, deviceId: String? ): Session { - val loginParams = if (Patterns.EMAIL_ADDRESS.matcher(login).matches()) { + val loginParams = if (login.isEmail()) { PasswordLoginParams.thirdPartyIdentifier( medium = ThreePidMedium.EMAIL, address = login, diff --git a/vector/src/main/java/im/vector/app/core/extensions/BasicExtensions.kt b/vector/src/main/java/im/vector/app/core/extensions/BasicExtensions.kt index 6bcbfe0ed5..692c85311a 100644 --- a/vector/src/main/java/im/vector/app/core/extensions/BasicExtensions.kt +++ b/vector/src/main/java/im/vector/app/core/extensions/BasicExtensions.kt @@ -21,6 +21,7 @@ import com.google.i18n.phonenumbers.NumberParseException import com.google.i18n.phonenumbers.PhoneNumberUtil import org.matrix.android.sdk.api.MatrixPatterns import org.matrix.android.sdk.api.extensions.ensurePrefix +import org.matrix.android.sdk.api.extensions.isEmail fun Boolean.toOnOff() = if (this) "ON" else "OFF" @@ -29,7 +30,7 @@ inline fun T.ooi(block: (T) -> Unit): T = also(block) /** * Check if a CharSequence is an email. */ -fun CharSequence.isEmail() = Patterns.EMAIL_ADDRESS.matcher(this).matches() +fun CharSequence.isEmail() = this.isEmail() fun CharSequence.isMatrixId() = MatrixPatterns.isUserId(this.toString()) From edf23bbb89be5e17e2506a52cb6e1ce01419241d Mon Sep 17 00:00:00 2001 From: yostyle Date: Mon, 23 Oct 2023 17:33:02 +0200 Subject: [PATCH 45/61] Add changelog Signed-off-by: yostyle --- changelog.d/8671.misc | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/8671.misc diff --git a/changelog.d/8671.misc b/changelog.d/8671.misc new file mode 100644 index 0000000000..345dc36fd6 --- /dev/null +++ b/changelog.d/8671.misc @@ -0,0 +1 @@ +Update regex for email address to be aligned on RFC 5322 \ No newline at end of file From 3d7489c7c59c366dd69bc540cf97ccfc1c01206d Mon Sep 17 00:00:00 2001 From: yostyle Date: Tue, 14 Nov 2023 12:35:33 +0100 Subject: [PATCH 46/61] Fix PR comment --- .../java/im/vector/app/core/extensions/BasicExtensions.kt | 7 ------- .../java/im/vector/app/features/command/CommandParser.kt | 2 +- .../features/login/LoginGenericTextInputFormFragment.kt | 2 +- .../app/features/login/LoginResetPasswordFragment.kt | 2 +- .../onboarding/ftueauth/FtueAuthEmailEntryFragment.kt | 2 +- .../ftueauth/FtueAuthGenericTextInputFormFragment.kt | 2 +- .../ftueauth/FtueAuthResetPasswordEmailEntryFragment.kt | 2 +- .../onboarding/ftueauth/FtueAuthResetPasswordFragment.kt | 2 +- .../settings/threepids/ThreePidsSettingsFragment.kt | 2 +- .../app/features/spaces/create/CreateSpaceViewModel.kt | 2 +- .../vector/app/features/userdirectory/UserListViewModel.kt | 2 +- 11 files changed, 10 insertions(+), 17 deletions(-) diff --git a/vector/src/main/java/im/vector/app/core/extensions/BasicExtensions.kt b/vector/src/main/java/im/vector/app/core/extensions/BasicExtensions.kt index 692c85311a..6fc8c3fa5f 100644 --- a/vector/src/main/java/im/vector/app/core/extensions/BasicExtensions.kt +++ b/vector/src/main/java/im/vector/app/core/extensions/BasicExtensions.kt @@ -16,22 +16,15 @@ package im.vector.app.core.extensions -import android.util.Patterns import com.google.i18n.phonenumbers.NumberParseException import com.google.i18n.phonenumbers.PhoneNumberUtil import org.matrix.android.sdk.api.MatrixPatterns import org.matrix.android.sdk.api.extensions.ensurePrefix -import org.matrix.android.sdk.api.extensions.isEmail fun Boolean.toOnOff() = if (this) "ON" else "OFF" inline fun T.ooi(block: (T) -> Unit): T = also(block) -/** - * Check if a CharSequence is an email. - */ -fun CharSequence.isEmail() = this.isEmail() - fun CharSequence.isMatrixId() = MatrixPatterns.isUserId(this.toString()) /** diff --git a/vector/src/main/java/im/vector/app/features/command/CommandParser.kt b/vector/src/main/java/im/vector/app/features/command/CommandParser.kt index 298387c324..c12f55814b 100644 --- a/vector/src/main/java/im/vector/app/features/command/CommandParser.kt +++ b/vector/src/main/java/im/vector/app/features/command/CommandParser.kt @@ -16,13 +16,13 @@ package im.vector.app.features.command -import im.vector.app.core.extensions.isEmail import im.vector.app.core.extensions.isMsisdn import im.vector.app.core.extensions.orEmpty import im.vector.app.features.home.room.detail.ChatEffect import im.vector.app.features.settings.VectorPreferences import org.matrix.android.sdk.api.MatrixPatterns import org.matrix.android.sdk.api.MatrixUrls.isMxcUrl +import org.matrix.android.sdk.api.extensions.isEmail import org.matrix.android.sdk.api.session.identity.ThreePid import timber.log.Timber import javax.inject.Inject diff --git a/vector/src/main/java/im/vector/app/features/login/LoginGenericTextInputFormFragment.kt b/vector/src/main/java/im/vector/app/features/login/LoginGenericTextInputFormFragment.kt index 2bc8419989..7e5adf273f 100644 --- a/vector/src/main/java/im/vector/app/features/login/LoginGenericTextInputFormFragment.kt +++ b/vector/src/main/java/im/vector/app/features/login/LoginGenericTextInputFormFragment.kt @@ -32,13 +32,13 @@ import com.google.i18n.phonenumbers.PhoneNumberUtil import dagger.hilt.android.AndroidEntryPoint import im.vector.app.R import im.vector.app.core.extensions.hideKeyboard -import im.vector.app.core.extensions.isEmail import im.vector.app.core.extensions.setTextOrHide import im.vector.app.databinding.FragmentLoginGenericTextInputFormBinding import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.parcelize.Parcelize import org.matrix.android.sdk.api.auth.registration.RegisterThreePid +import org.matrix.android.sdk.api.extensions.isEmail import org.matrix.android.sdk.api.failure.Failure import org.matrix.android.sdk.api.failure.is401 import reactivecircus.flowbinding.android.widget.textChanges diff --git a/vector/src/main/java/im/vector/app/features/login/LoginResetPasswordFragment.kt b/vector/src/main/java/im/vector/app/features/login/LoginResetPasswordFragment.kt index 87df2d9483..a8bbbdde0b 100644 --- a/vector/src/main/java/im/vector/app/features/login/LoginResetPasswordFragment.kt +++ b/vector/src/main/java/im/vector/app/features/login/LoginResetPasswordFragment.kt @@ -28,7 +28,6 @@ import dagger.hilt.android.AndroidEntryPoint import im.vector.app.R import im.vector.app.core.extensions.hideKeyboard import im.vector.app.core.extensions.hidePassword -import im.vector.app.core.extensions.isEmail import im.vector.app.core.extensions.toReducedUrl import im.vector.app.databinding.FragmentLoginResetPasswordBinding import im.vector.app.features.analytics.plan.MobileScreen @@ -36,6 +35,7 @@ import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach +import org.matrix.android.sdk.api.extensions.isEmail import reactivecircus.flowbinding.android.widget.textChanges /** diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthEmailEntryFragment.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthEmailEntryFragment.kt index e315f191c1..39430a0d20 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthEmailEntryFragment.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthEmailEntryFragment.kt @@ -30,7 +30,6 @@ import im.vector.app.core.extensions.clearErrorOnChange import im.vector.app.core.extensions.content import im.vector.app.core.extensions.editText import im.vector.app.core.extensions.hasContent -import im.vector.app.core.extensions.isEmail import im.vector.app.core.extensions.setOnImeDoneListener import im.vector.app.core.extensions.toReducedUrl import im.vector.app.databinding.FragmentFtueEmailInputBinding @@ -39,6 +38,7 @@ import im.vector.app.features.onboarding.OnboardingViewState import im.vector.app.features.onboarding.RegisterAction import kotlinx.parcelize.Parcelize import org.matrix.android.sdk.api.auth.registration.RegisterThreePid +import org.matrix.android.sdk.api.extensions.isEmail @Parcelize data class FtueAuthEmailEntryFragmentArgument( diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthGenericTextInputFormFragment.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthGenericTextInputFormFragment.kt index 02d0c25cfd..3c322f81fc 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthGenericTextInputFormFragment.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthGenericTextInputFormFragment.kt @@ -32,7 +32,6 @@ import com.google.i18n.phonenumbers.PhoneNumberUtil import dagger.hilt.android.AndroidEntryPoint import im.vector.app.R import im.vector.app.core.extensions.hideKeyboard -import im.vector.app.core.extensions.isEmail import im.vector.app.core.extensions.setTextOrHide import im.vector.app.databinding.FragmentLoginGenericTextInputFormBinding import im.vector.app.features.login.TextInputFormFragmentMode @@ -42,6 +41,7 @@ import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.parcelize.Parcelize import org.matrix.android.sdk.api.auth.registration.RegisterThreePid +import org.matrix.android.sdk.api.extensions.isEmail import org.matrix.android.sdk.api.failure.Failure import org.matrix.android.sdk.api.failure.is401 import reactivecircus.flowbinding.android.widget.textChanges diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthResetPasswordEmailEntryFragment.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthResetPasswordEmailEntryFragment.kt index 51c73a40e3..c24f068466 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthResetPasswordEmailEntryFragment.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthResetPasswordEmailEntryFragment.kt @@ -25,12 +25,12 @@ import im.vector.app.R import im.vector.app.core.extensions.associateContentStateWith import im.vector.app.core.extensions.clearErrorOnChange import im.vector.app.core.extensions.content -import im.vector.app.core.extensions.isEmail import im.vector.app.core.extensions.setOnImeDoneListener import im.vector.app.core.extensions.toReducedUrl import im.vector.app.databinding.FragmentFtueResetPasswordEmailInputBinding import im.vector.app.features.onboarding.OnboardingAction import im.vector.app.features.onboarding.OnboardingViewState +import org.matrix.android.sdk.api.extensions.isEmail @AndroidEntryPoint class FtueAuthResetPasswordEmailEntryFragment : diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthResetPasswordFragment.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthResetPasswordFragment.kt index 376218d474..70197cdcdb 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthResetPasswordFragment.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthResetPasswordFragment.kt @@ -26,7 +26,6 @@ import dagger.hilt.android.AndroidEntryPoint import im.vector.app.R import im.vector.app.core.extensions.hideKeyboard import im.vector.app.core.extensions.hidePassword -import im.vector.app.core.extensions.isEmail import im.vector.app.core.extensions.toReducedUrl import im.vector.app.databinding.FragmentLoginResetPasswordBinding import im.vector.app.features.onboarding.OnboardingAction @@ -35,6 +34,7 @@ import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach +import org.matrix.android.sdk.api.extensions.isEmail import reactivecircus.flowbinding.android.widget.textChanges /** diff --git a/vector/src/main/java/im/vector/app/features/settings/threepids/ThreePidsSettingsFragment.kt b/vector/src/main/java/im/vector/app/features/settings/threepids/ThreePidsSettingsFragment.kt index ae3cbcc9ca..7fe4fad844 100644 --- a/vector/src/main/java/im/vector/app/features/settings/threepids/ThreePidsSettingsFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/threepids/ThreePidsSettingsFragment.kt @@ -31,7 +31,6 @@ import im.vector.app.core.extensions.cleanup import im.vector.app.core.extensions.configureWith import im.vector.app.core.extensions.getFormattedValue import im.vector.app.core.extensions.hideKeyboard -import im.vector.app.core.extensions.isEmail import im.vector.app.core.extensions.isMsisdn import im.vector.app.core.extensions.registerStartForActivityResult import im.vector.app.core.platform.OnBackPressed @@ -39,6 +38,7 @@ import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.databinding.FragmentGenericRecyclerBinding import im.vector.app.features.auth.ReAuthActivity import org.matrix.android.sdk.api.auth.data.LoginFlowTypes +import org.matrix.android.sdk.api.extensions.isEmail import org.matrix.android.sdk.api.session.identity.ThreePid import javax.inject.Inject diff --git a/vector/src/main/java/im/vector/app/features/spaces/create/CreateSpaceViewModel.kt b/vector/src/main/java/im/vector/app/features/spaces/create/CreateSpaceViewModel.kt index 1cfac4a5fe..2d72999a03 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/create/CreateSpaceViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/create/CreateSpaceViewModel.kt @@ -29,7 +29,6 @@ import im.vector.app.R import im.vector.app.core.di.MavericksAssistedViewModelFactory import im.vector.app.core.di.hiltMavericksViewModelFactory import im.vector.app.core.error.ErrorFormatter -import im.vector.app.core.extensions.isEmail import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.resources.StringProvider import im.vector.app.features.analytics.AnalyticsTracker @@ -38,6 +37,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import org.matrix.android.sdk.api.MatrixPatterns import org.matrix.android.sdk.api.MatrixPatterns.getServerName +import org.matrix.android.sdk.api.extensions.isEmail import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.identity.IdentityServiceListener import org.matrix.android.sdk.api.session.room.AliasAvailabilityResult diff --git a/vector/src/main/java/im/vector/app/features/userdirectory/UserListViewModel.kt b/vector/src/main/java/im/vector/app/features/userdirectory/UserListViewModel.kt index 96875d73a5..729790f071 100644 --- a/vector/src/main/java/im/vector/app/features/userdirectory/UserListViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/userdirectory/UserListViewModel.kt @@ -26,7 +26,6 @@ import dagger.assisted.AssistedInject import im.vector.app.R import im.vector.app.core.di.MavericksAssistedViewModelFactory import im.vector.app.core.di.hiltMavericksViewModelFactory -import im.vector.app.core.extensions.isEmail import im.vector.app.core.extensions.toggle import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.resources.StringProvider @@ -43,6 +42,7 @@ import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.sample import kotlinx.coroutines.launch import org.matrix.android.sdk.api.MatrixPatterns +import org.matrix.android.sdk.api.extensions.isEmail import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.raw.RawService import org.matrix.android.sdk.api.session.Session From 7c58af735b6434fa890b8251d700bf413c78839a Mon Sep 17 00:00:00 2001 From: Giom Foret Date: Wed, 15 Nov 2023 17:10:44 +0100 Subject: [PATCH 47/61] Unified Push: Ignore the potential SSL error when the custom gateway is testing When the Unified Push is enabled, the application checks the potential custom gateway before applying it. If an SSL error happens, the application may ignore this error and keep using this custom gateway. The actual SSL check will be done server side where this gateway is actually used. --- .../vector/app/core/pushers/UnifiedPushHelper.kt | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt b/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt index 77fed03c8a..07052c7146 100644 --- a/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt +++ b/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt @@ -26,11 +26,13 @@ import im.vector.app.core.resources.StringProvider import im.vector.app.core.utils.getApplicationLabel import org.matrix.android.sdk.api.Matrix import org.matrix.android.sdk.api.cache.CacheStrategy +import org.matrix.android.sdk.api.failure.Failure import org.matrix.android.sdk.api.util.MatrixJsonParser import org.unifiedpush.android.connector.UnifiedPush import timber.log.Timber import java.net.URL import javax.inject.Inject +import javax.net.ssl.SSLHandshakeException class UnifiedPushHelper @Inject constructor( private val context: Context, @@ -104,7 +106,11 @@ class UnifiedPushHelper @Inject constructor( // else, unifiedpush, and pushkey is an endpoint val gateway = stringProvider.getString(R.string.default_push_gateway_http_url) val parsed = URL(endpoint) - val port = if (parsed.port != -1) { ":${parsed.port}" } else { "" } + val port = if (parsed.port != -1) { + ":${parsed.port}" + } else { + "" + } val custom = "${parsed.protocol}://${parsed.host}${port}/_matrix/push/v1/notify" Timber.i("Testing $custom") try { @@ -120,7 +126,13 @@ class UnifiedPushHelper @Inject constructor( } } } catch (e: Throwable) { - Timber.d(e, "Cannot try custom gateway") + Timber.e(e, "Cannot try custom gateway") + if (e is Failure.NetworkConnection && e.ioException is SSLHandshakeException) { + Timber.w(e, "SSLHandshakeException, ignore this error") + unifiedPushStore.storePushGateway(custom) + onDoneRunnable?.run() + return + } } unifiedPushStore.storePushGateway(gateway) onDoneRunnable?.run() From ee2fd9f1239790c80978f555ebc6d223968df89c Mon Sep 17 00:00:00 2001 From: Giom Foret Date: Wed, 15 Nov 2023 17:20:37 +0100 Subject: [PATCH 48/61] add changelog --- changelog.d/8683.bugfix | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/8683.bugfix diff --git a/changelog.d/8683.bugfix b/changelog.d/8683.bugfix new file mode 100644 index 0000000000..5ce4bf96b8 --- /dev/null +++ b/changelog.d/8683.bugfix @@ -0,0 +1 @@ +Unified Push: Ignore the potential SSL error when the custom gateway is testing locally From 63ef40f58b7717ea79ddd8260f22726bd5809594 Mon Sep 17 00:00:00 2001 From: jonnyandrew Date: Fri, 17 Nov 2023 17:09:18 +0000 Subject: [PATCH 49/61] Fix issue with timeline message view reuse while rich text editor is enabled (#8688) --- changelog.d/8688.bugfix | 1 + .../detail/timeline/item/MessageTextItem.kt | 27 ++++++++++++++----- 2 files changed, 22 insertions(+), 6 deletions(-) create mode 100644 changelog.d/8688.bugfix diff --git a/changelog.d/8688.bugfix b/changelog.d/8688.bugfix new file mode 100644 index 0000000000..0f746f1293 --- /dev/null +++ b/changelog.d/8688.bugfix @@ -0,0 +1 @@ +Fix issue with timeline message view reuse while rich text editor is enabled \ No newline at end of file diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageTextItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageTextItem.kt index a9cd25ae19..6ffaa6d896 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageTextItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageTextItem.kt @@ -85,8 +85,13 @@ abstract class MessageTextItem : AbsMessageItem() { } holder.previewUrlView.delegate = previewUrlCallback holder.previewUrlView.renderMessageLayout(attributes.informationData.messageLayout) - - val messageView: AppCompatTextView = if (useRichTextEditorStyle) holder.richMessageView else holder.plainMessageView + if (useRichTextEditorStyle) { + holder.plainMessageView?.isVisible = false + } else { + holder.richMessageView?.isVisible = false + } + val messageView: AppCompatTextView = if (useRichTextEditorStyle) holder.requireRichMessageView() else holder.requirePlainMessageView() + messageView.isVisible = true if (useBigFont) { messageView.textSize = 44F } else { @@ -133,11 +138,21 @@ abstract class MessageTextItem : AbsMessageItem() { val previewUrlView by bind(R.id.messageUrlPreview) private val richMessageStub by bind(R.id.richMessageTextViewStub) private val plainMessageStub by bind(R.id.plainMessageTextViewStub) - val richMessageView: AppCompatTextView by lazy { - richMessageStub.inflate().findViewById(R.id.messageTextView) + var richMessageView: AppCompatTextView? = null + private set + var plainMessageView: AppCompatTextView? = null + private set + + fun requireRichMessageView(): AppCompatTextView { + val view = richMessageView ?: richMessageStub.inflate().findViewById(R.id.messageTextView) + richMessageView = view + return view } - val plainMessageView: AppCompatTextView by lazy { - plainMessageStub.inflate().findViewById(R.id.messageTextView) + + fun requirePlainMessageView(): AppCompatTextView { + val view = plainMessageView ?: plainMessageStub.inflate().findViewById(R.id.messageTextView) + plainMessageView = view + return view } } From 84158ece370d543c32718f5a4a669b1323953b79 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 7 Nov 2023 16:35:12 +0100 Subject: [PATCH 50/61] Ensure Background sync is not stopped when there is an active call. It was happening since the application is foregrounded when VectorCallActivity is displayed. --- changelog.d/4066.bugfix | 1 + .../java/im/vector/app/VectorApplication.kt | 24 +++++++++++++++---- .../features/call/webrtc/WebRtcCallManager.kt | 6 ++++- 3 files changed, 25 insertions(+), 6 deletions(-) create mode 100644 changelog.d/4066.bugfix diff --git a/changelog.d/4066.bugfix b/changelog.d/4066.bugfix new file mode 100644 index 0000000000..7c97993511 --- /dev/null +++ b/changelog.d/4066.bugfix @@ -0,0 +1 @@ +Stop incoming call ringing if the call is cancelled or answered on another session. diff --git a/vector-app/src/main/java/im/vector/app/VectorApplication.kt b/vector-app/src/main/java/im/vector/app/VectorApplication.kt index 8d12292524..deb3d8d3aa 100644 --- a/vector-app/src/main/java/im/vector/app/VectorApplication.kt +++ b/vector-app/src/main/java/im/vector/app/VectorApplication.kt @@ -108,6 +108,7 @@ class VectorApplication : @Inject lateinit var buildMeta: BuildMeta @Inject lateinit var leakDetector: LeakDetector @Inject lateinit var vectorLocale: VectorLocale + @Inject lateinit var webRtcCallManager: WebRtcCallManager // font thread handler private var fontThreadHandler: Handler? = null @@ -167,20 +168,33 @@ class VectorApplication : notificationUtils.createNotificationChannels() ProcessLifecycleOwner.get().lifecycle.addObserver(object : DefaultLifecycleObserver { + private var stopBackgroundSync = false + override fun onResume(owner: LifecycleOwner) { Timber.i("App entered foreground") fcmHelper.onEnterForeground(activeSessionHolder) - activeSessionHolder.getSafeActiveSessionAsync { - it?.syncService()?.stopAnyBackgroundSync() + if (webRtcCallManager.currentCall.get() == null) { + Timber.i("App entered foreground and no active call: stop any background sync") + activeSessionHolder.getSafeActiveSessionAsync { + it?.syncService()?.stopAnyBackgroundSync() + } + } else { + Timber.i("App entered foreground: there is an active call, set stopBackgroundSync to true") + stopBackgroundSync = true } -// activeSessionHolder.getSafeActiveSession()?.also { -// it.syncService().stopAnyBackgroundSync() -// } } override fun onPause(owner: LifecycleOwner) { Timber.i("App entered background") fcmHelper.onEnterBackground(activeSessionHolder) + + if (stopBackgroundSync) { + Timber.i("App entered background: stop any background sync") + activeSessionHolder.getSafeActiveSessionAsync { + it?.syncService()?.stopAnyBackgroundSync() + } + stopBackgroundSync = false + } } }) ProcessLifecycleOwner.get().lifecycle.addObserver(spaceStateHandler) diff --git a/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCallManager.kt b/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCallManager.kt index 1eb4134a87..c432e7ebd4 100644 --- a/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCallManager.kt +++ b/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCallManager.kt @@ -139,6 +139,7 @@ class WebRtcCallManager @Inject constructor( private val rootEglBase by lazy { EglUtils.rootEglBase } private var isInBackground: Boolean = true + private var syncStartedWhenInBackground: Boolean = false override fun onResume(owner: LifecycleOwner) { isInBackground = false @@ -274,13 +275,15 @@ class WebRtcCallManager @Inject constructor( peerConnectionFactory = null audioManager.setMode(CallAudioManager.Mode.DEFAULT) // did we start background sync? so we should stop it - if (isInBackground) { + if (syncStartedWhenInBackground) { if (!unifiedPushHelper.isBackgroundSync()) { + Timber.tag(loggerTag.value).v("Sync started when in background, stop it") currentSession?.syncService()?.stopAnyBackgroundSync() } else { // for fdroid we should not stop, it should continue syncing // maybe we should restore default timeout/delay though? } + syncStartedWhenInBackground = false } } } @@ -383,6 +386,7 @@ class WebRtcCallManager @Inject constructor( if (isInBackground) { if (!unifiedPushHelper.isBackgroundSync()) { // only for push version as fdroid version is already doing it? + syncStartedWhenInBackground = true currentSession?.syncService()?.startAutomaticBackgroundSync(30, 0) } else { // Maybe increase sync freq? but how to set back to default values? From 8d85d047b7328f082da8d69fd09539ce7b893490 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 22 Nov 2023 09:42:39 +0100 Subject: [PATCH 51/61] Ensure Background sync is not stopped when there is an active call, even when the app goes to background. --- .../src/main/java/im/vector/app/VectorApplication.kt | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/vector-app/src/main/java/im/vector/app/VectorApplication.kt b/vector-app/src/main/java/im/vector/app/VectorApplication.kt index deb3d8d3aa..7b41c12773 100644 --- a/vector-app/src/main/java/im/vector/app/VectorApplication.kt +++ b/vector-app/src/main/java/im/vector/app/VectorApplication.kt @@ -189,11 +189,15 @@ class VectorApplication : fcmHelper.onEnterBackground(activeSessionHolder) if (stopBackgroundSync) { - Timber.i("App entered background: stop any background sync") - activeSessionHolder.getSafeActiveSessionAsync { - it?.syncService()?.stopAnyBackgroundSync() + if (webRtcCallManager.currentCall.get() == null) { + Timber.i("App entered background: stop any background sync") + activeSessionHolder.getSafeActiveSessionAsync { + it?.syncService()?.stopAnyBackgroundSync() + } + stopBackgroundSync = false + } else { + Timber.i("App entered background: there is an active call do not stop background sync") } - stopBackgroundSync = false } } }) From 768385a375131b303efb83e7c71b4f0248a9f319 Mon Sep 17 00:00:00 2001 From: Yoan Pintas Date: Wed, 22 Nov 2023 21:11:30 +0100 Subject: [PATCH 52/61] Fix crash in room creation screen (#990) --- changelog.d/885.bugfix | 1 + .../roomdirectory/createroom/TchapRoomAvatarWithNameItem.kt | 5 ----- 2 files changed, 1 insertion(+), 5 deletions(-) create mode 100644 changelog.d/885.bugfix diff --git a/changelog.d/885.bugfix b/changelog.d/885.bugfix new file mode 100644 index 0000000000..6f75d6cbc0 --- /dev/null +++ b/changelog.d/885.bugfix @@ -0,0 +1 @@ +Correction du crash du nommage du salon lors de sa création. \ No newline at end of file diff --git a/vector/src/main/java/fr/gouv/tchap/features/home/roomdirectory/createroom/TchapRoomAvatarWithNameItem.kt b/vector/src/main/java/fr/gouv/tchap/features/home/roomdirectory/createroom/TchapRoomAvatarWithNameItem.kt index 725d283631..e4cdd05772 100644 --- a/vector/src/main/java/fr/gouv/tchap/features/home/roomdirectory/createroom/TchapRoomAvatarWithNameItem.kt +++ b/vector/src/main/java/fr/gouv/tchap/features/home/roomdirectory/createroom/TchapRoomAvatarWithNameItem.kt @@ -18,7 +18,6 @@ package fr.gouv.tchap.features.home.roomdirectory.createroom import android.net.Uri import android.text.Editable import android.view.View -import android.view.inputmethod.EditorInfo import android.widget.ImageView import android.widget.TextView import androidx.core.view.isVisible @@ -80,9 +79,6 @@ abstract class TchapRoomAvatarWithNameItem : EpoxyModelWithHolder Date: Thu, 23 Nov 2023 16:05:15 +0100 Subject: [PATCH 53/61] Update dev endpoints (#991) --- changelog.d/991.improvements | 1 + vector-config/src/devTchap/res/values/config.xml | 14 +++++++------- 2 files changed, 8 insertions(+), 7 deletions(-) create mode 100644 changelog.d/991.improvements diff --git a/changelog.d/991.improvements b/changelog.d/991.improvements new file mode 100644 index 0000000000..166da4e345 --- /dev/null +++ b/changelog.d/991.improvements @@ -0,0 +1 @@ +Mise à jour des endpoints pour la version de dev diff --git a/vector-config/src/devTchap/res/values/config.xml b/vector-config/src/devTchap/res/values/config.xml index 60378a67c8..7a7606622b 100644 --- a/vector-config/src/devTchap/res/values/config.xml +++ b/vector-config/src/devTchap/res/values/config.xml @@ -3,22 +3,22 @@ - agent1.tchap.incubateur.net - agent2.tchap.incubateur.net + dev01.tchap.incubateur.net + dev02.tchap.incubateur.net - externe.tchap.incubateur.net + ext01.tchap.incubateur.net - agent1.tchap.incubateur.net - agent2.tchap.incubateur.net + dev01.tchap.incubateur.net + dev02.tchap.incubateur.net - agent1.tchap.incubateur.net + dev01.tchap.incubateur.net Le document (%s) a été filtré par la politique de sécurité Le fichier est infecté, impossible de le télécharger. + Pour des raisons de sécurité l’installation d’application à partir de ${app_name} n’est pas autorisée. Convertir en salon privé (irréversible) diff --git a/library/ui-strings/src/main/res/values/strings_tchap.xml b/library/ui-strings/src/main/res/values/strings_tchap.xml index 9aabed5a49..c3abf1324e 100644 --- a/library/ui-strings/src/main/res/values/strings_tchap.xml +++ b/library/ui-strings/src/main/res/values/strings_tchap.xml @@ -50,6 +50,7 @@ The file (%s) has been blocked We have detected that this file is infected, so it cannot be downloaded. + For security reasons, installing applications from ${app_name} is not allowed. Convert to private room (irreversible) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt index 3926766263..708a2930f7 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt @@ -628,13 +628,23 @@ class TimelineFragment : } private fun startOpenFileIntent(action: RoomDetailViewEvents.OpenFile) { - if (action.mimeType == MimeTypes.Apk) { - installApk(action) + // Tchap: Remove the ability to install an app from Tchap (https://github.com/tchapgouv/tchap-android/issues/832) + if (action.mimeType in listOf(MimeTypes.Apk, "application/x-authorware-bin")) { + showApkAlert() } else { openFile(action) } } + private fun showApkAlert() { + MaterialAlertDialogBuilder(requireContext()) + .setTitle(getString(R.string.dialog_title_warning)) + .setMessage(getString(R.string.tchap_scan_media_warning_apk)) + .setCancelable(false) + .setPositiveButton(R.string.ok, null) + .show() + } + private fun openFile(action: RoomDetailViewEvents.OpenFile) { val intent = Intent(Intent.ACTION_VIEW).apply { setDataAndTypeAndNormalize(action.uri, action.mimeType) From fbb8205701386d5bdb271f43c8227fd1766de8b8 Mon Sep 17 00:00:00 2001 From: Yoan Pintas Date: Mon, 18 Dec 2023 10:21:09 +0000 Subject: [PATCH 60/61] Une surprise vous attend dans cette version (#994) --- .../src/main/res/values-fr/strings_tchap.xml | 1 + .../src/main/res/values/strings_tchap.xml | 1 + .../features/settings/VectorPreferences.kt | 1 + .../settings/VectorSettingsRootFragment.kt | 25 +++++++++++++++++++ .../src/main/res/xml/vector_settings_root.xml | 6 +++++ 5 files changed, 34 insertions(+) diff --git a/library/ui-strings/src/main/res/values-fr/strings_tchap.xml b/library/ui-strings/src/main/res/values-fr/strings_tchap.xml index e74e3f006f..fc220a367d 100644 --- a/library/ui-strings/src/main/res/values-fr/strings_tchap.xml +++ b/library/ui-strings/src/main/res/values-fr/strings_tchap.xml @@ -111,6 +111,7 @@ Inscrire mon compte sur liste rouge Les autres utilisateurs ne pourront pas découvrir mon compte lors de leurs recherches Pour désactiver cette option, vous devez accepter que votre adresse e\u2011mail soit visible des autres utilisateurs lors de leurs recherches. + 🎄 Joyeuses Fêtes 🎄 La vérification de votre nouvelle session a échoué. diff --git a/library/ui-strings/src/main/res/values/strings_tchap.xml b/library/ui-strings/src/main/res/values/strings_tchap.xml index c3abf1324e..89830b66b3 100644 --- a/library/ui-strings/src/main/res/values/strings_tchap.xml +++ b/library/ui-strings/src/main/res/values/strings_tchap.xml @@ -111,6 +111,7 @@ Subscribe to the red list. "Other users won't be able to find my account in their search results." To disable this option, you must accept that your email address is visible to the other users. + 🎄 Happy Holidays 🎄 Failed to verify your new session. diff --git a/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt b/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt index ef3e791d91..7cf681e2d6 100755 --- a/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt +++ b/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt @@ -227,6 +227,7 @@ class VectorPreferences @Inject constructor( private const val DID_ASK_TO_ENABLE_SESSION_PUSH = "DID_ASK_TO_ENABLE_SESSION_PUSH" const val TCHAP_SETTINGS_HIDE_FROM_USERS_DIRECTORY_PREFERENCE_KEY = "TCHAP_SETTINGS_HIDE_FROM_USERS_DIRECTORY_PREFERENCE_KEY" + const val TCHAP_SETTINGS_CHRISTMAS_PREFERENCE_KEY = "TCHAP_SETTINGS_CHRISTMAS_PREFERENCE_KEY" // Location Sharing const val SETTINGS_PREF_ENABLE_LOCATION_SHARING = "SETTINGS_PREF_ENABLE_LOCATION_SHARING" diff --git a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsRootFragment.kt b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsRootFragment.kt index ff376aefa8..be6a96cd8f 100644 --- a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsRootFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsRootFragment.kt @@ -16,14 +16,18 @@ package im.vector.app.features.settings +import android.content.Context import android.os.Bundle import androidx.preference.Preference import dagger.hilt.android.AndroidEntryPoint import im.vector.app.R +import im.vector.app.core.extensions.singletonEntryPoint import im.vector.app.core.preference.VectorPreference import im.vector.app.core.utils.FirstThrottler import im.vector.app.core.utils.openUrlInChromeCustomTab import im.vector.app.features.analytics.plan.MobileScreen +import im.vector.app.features.navigation.Navigator +import java.util.Calendar @AndroidEntryPoint class VectorSettingsRootFragment : @@ -34,6 +38,14 @@ class VectorSettingsRootFragment : private val firstThrottler = FirstThrottler(1000) + private lateinit var navigator: Navigator + + override fun onAttach(context: Context) { + val singletonEntryPoint = context.singletonEntryPoint() + navigator = singletonEntryPoint.navigator() + super.onAttach(context) + } + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) analyticsScreenName = MobileScreen.ScreenName.Settings @@ -50,6 +62,19 @@ class VectorSettingsRootFragment : } false } + + // Tchap: Manage Christmas entry + if (Calendar.getInstance().before(Calendar.getInstance().apply { set(2024, 1, 10) })) { + findPreference(VectorPreferences.TCHAP_SETTINGS_CHRISTMAS_PREFERENCE_KEY)!!.let { + it.isVisible = true + it.onPreferenceClickListener = Preference.OnPreferenceClickListener { + if (firstThrottler.canHandle() is FirstThrottler.CanHandlerResult.Yes) { + navigator.openRoom(requireContext(), "!cDKdQyXHeWBEaKDWWV:agent.dinum.tchap.gouv.fr", null) + } + false + } + } + } } private fun tintIcons() { diff --git a/vector/src/main/res/xml/vector_settings_root.xml b/vector/src/main/res/xml/vector_settings_root.xml index 13a3022233..44064f2e60 100644 --- a/vector/src/main/res/xml/vector_settings_root.xml +++ b/vector/src/main/res/xml/vector_settings_root.xml @@ -62,4 +62,10 @@ app:fragment="im.vector.app.features.settings.legals.LegalsFragment" app:isPreferenceVisible="@bool/settings_root_legals_visible" /> + + From 0908821f968bc649f1baa602f96d2aa782a97b89 Mon Sep 17 00:00:00 2001 From: yostyle Date: Mon, 18 Dec 2023 16:01:31 +0100 Subject: [PATCH 61/61] Upgrade version to 2.9.7 --- TCHAP_CHANGES.md | 14 ++++++++++++++ changelog.d/885.bugfix | 1 - changelog.d/991.improvements | 1 - changelog.d/995.bugfix | 1 - changelog.d/998.improvements | 1 - towncrier.toml | 2 +- vector-app/build.gradle | 2 +- 7 files changed, 16 insertions(+), 6 deletions(-) delete mode 100644 changelog.d/885.bugfix delete mode 100644 changelog.d/991.improvements delete mode 100644 changelog.d/995.bugfix delete mode 100644 changelog.d/998.improvements diff --git a/TCHAP_CHANGES.md b/TCHAP_CHANGES.md index 1d1e4cbce7..5f85208059 100644 --- a/TCHAP_CHANGES.md +++ b/TCHAP_CHANGES.md @@ -1,3 +1,17 @@ +Changes in Tchap 2.9.7 (2023-12-18) +=================================== + +Improvements 🙌 +-------------- + - Mise à jour des endpoints pour la version de dev ([#991](https://github.com/tchapgouv/tchap-android/issues/991)) + - Rebase against Element-Android v1.6.8 ([#998](https://github.com/tchapgouv/tchap-android/issues/998)) + +Bugfixes 🐛 +---------- + - Correction du crash du nommage du salon lors de sa création. ([#885](https://github.com/tchapgouv/tchap-android/issues/885)) + - L'ouverture d'une pièce jointe (fichier. apk) entraîne la fermeture de l'application. ([#995](https://github.com/tchapgouv/tchap-android/issues/995)) + + Changes in Tchap 2.9.6 (2023-10-30) =================================== diff --git a/changelog.d/885.bugfix b/changelog.d/885.bugfix deleted file mode 100644 index 6f75d6cbc0..0000000000 --- a/changelog.d/885.bugfix +++ /dev/null @@ -1 +0,0 @@ -Correction du crash du nommage du salon lors de sa création. \ No newline at end of file diff --git a/changelog.d/991.improvements b/changelog.d/991.improvements deleted file mode 100644 index 166da4e345..0000000000 --- a/changelog.d/991.improvements +++ /dev/null @@ -1 +0,0 @@ -Mise à jour des endpoints pour la version de dev diff --git a/changelog.d/995.bugfix b/changelog.d/995.bugfix deleted file mode 100644 index 5ff222926e..0000000000 --- a/changelog.d/995.bugfix +++ /dev/null @@ -1 +0,0 @@ -L'ouverture d'une pièce jointe (fichier. apk) entraîne la fermeture de l'application. \ No newline at end of file diff --git a/changelog.d/998.improvements b/changelog.d/998.improvements deleted file mode 100644 index 6535d16a39..0000000000 --- a/changelog.d/998.improvements +++ /dev/null @@ -1 +0,0 @@ -Rebase against Element-Android v1.6.8 \ No newline at end of file diff --git a/towncrier.toml b/towncrier.toml index 9cf39c6d12..aeae552313 100644 --- a/towncrier.toml +++ b/towncrier.toml @@ -1,5 +1,5 @@ [tool.towncrier] - version = "2.9.6" + version = "2.9.7" directory = "changelog.d" filename = "TCHAP_CHANGES.md" name = "Changes in Tchap" diff --git a/vector-app/build.gradle b/vector-app/build.gradle index cea16e80ae..348d910f02 100644 --- a/vector-app/build.gradle +++ b/vector-app/build.gradle @@ -37,7 +37,7 @@ ext.versionMinor = 9 // Note: even values are reserved for regular release, odd values for hotfix release. // When creating a hotfix, you should decrease the value, since the current value // is the value for the next regular release. -ext.versionPatch = 6 +ext.versionPatch = 7 static def getGitTimestamp() { def cmd = 'git show -s --format=%ct'