Beispiel #1
0
        private static int CleanOTPDB(PwDatabase db)
        {
            //Get DB to work on
            PwDatabase otpdb = db;

            OTPDAO.OTPHandler_DB h = GetOTPHandler(db);
            if (h == null)
            {
                return(0);
            }
            {
                if (!h.EnsureOTPUsagePossible(null))
                {
                    return(-1);
                }
                otpdb = h.OTPDB;
            }
            List <PwEntry> lEntries = otpdb.RootGroup.GetEntries(true).Where(x => !x.Strings.Exists(Config.OTPFIELD)).ToList();

            foreach (PwEntry pe in lEntries)
            {
                otpdb.RootGroup.Entries.Remove(pe);
            }
            return(lEntries.Count);
        }
Beispiel #2
0
        private static int SanitizeSeeds(PwDatabase db)
        {
            //Get DB to work on
            PwDatabase otpdb = db;

            OTPDAO.OTPHandler_DB h = GetOTPHandler(db);
            if (h != null)
            {
                if (!h.EnsureOTPUsagePossible(null))
                {
                    return(-1);
                }
                otpdb = h.OTPDB;
            }
            int i = 0;

            foreach (PwEntry pe in otpdb.RootGroup.GetEntries(true))
            {
                KPOTP otp = OTPDAO.GetOTP(pe);
                if (!otp.Valid)
                {
                    continue;
                }
                otp.OTPSeed = otp.OTPSeed;
                if (otp.SanitizeChanged)
                {
                    i++;
                    pe.CreateBackup(otpdb);
                    pe.Strings.Set(Config.OTPFIELD, otp.OTPAuthString);
                }
            }
            return(i);
        }
Beispiel #3
0
 private void RefreshHandler(PwDatabase db)
 {
     Config.UseDBForOTPSeeds(db, cbUseDBForSeeds.Checked);
     Config.PreloadOTPDB(db, cbPreloadOTP.Checked);
     m_handler = OTPDAO.GetOTPHandler(db) as OTPDAO.OTPHandler_DB;
     if (m_handler == null)
     {
         m_handler = new OTPDAO.OTPHandler_DB();
         m_handler.SetDB(db, false);
     }
 }
Beispiel #4
0
        private void bMigrate_Click(object sender, EventArgs e)
        {
            PwDatabase db            = m_dDB.ElementAt(lbDB.SelectedIndex).Key;
            bool       bToKeePassOTP = cbMigrate.SelectedItem.ToString().EndsWith("KeePassOTP");
            string     sPlugin       = PluginTranslate.PluginName;

            MigrationBase mBase = null;
            string        sel   = cbMigrate.SelectedItem.ToString();

            foreach (KeyValuePair <string, MigrationBase> kvp in m_dMigration)
            {
                if (sel.Contains(kvp.Key))
                {
                    mBase = kvp.Value;
                    if (bToKeePassOTP)
                    {
                        sPlugin = kvp.Key;
                    }
                    break;
                }
            }
            if (mBase == null)
            {
                return;
            }

            bool bRemove = Tools.AskYesNo(string.Format(PluginTranslate.MigrateOtherPlugins_Delete, sPlugin)) == DialogResult.Yes;

            int EntriesOverall;
            int EntriesMigrated;

            mBase.SetDB(db);
            if (bToKeePassOTP)
            {
                mBase.MigrateToKeePassOTP(bRemove, out EntriesOverall, out EntriesMigrated);
            }
            else
            {
                mBase.MigrateFromKeePassOTP(bRemove, out EntriesOverall, out EntriesMigrated);
            }

            if ((EntriesMigrated != -1) && (EntriesOverall != -1))
            {
                OTPDAO.OTPHandler_DB h = OTPDAO.GetOTPHandler(db) as OTPDAO.OTPHandler_DB;
                if (h != null)
                {
                    h.OTPDB_SaveAfterMigration();
                }
                Tools.ShowInfo(string.Format(PluginTranslate.MigrateOtherPlugins_Result, EntriesMigrated, EntriesOverall));
            }
        }
Beispiel #5
0
        private void cbUseDBForSeeds_CheckedChanged(object sender, EventArgs e)
        {
            PwDatabase db = m_dDB.ElementAt(lbDB.SelectedIndex).Key;

            if (!cbUseDBForSeeds.Checked)
            {
                OTPDAO.OTPHandler_DB h = OTPDAO.GetOTPHandler(db);
                if ((h != null) && h.OTPDB_Exists)
                {
                    DialogResult dr = DialogResult.None;
                    if (!h.HasEntries())
                    {
                        dr = DialogResult.Yes;
                    }
                    else
                    {
                        dr = MessageBox.Show(string.Format(PluginTranslate.ConfirmOTPDBDelete, DialogResult.Yes.ToString(), DialogResult.No.ToString()),
                                             PluginTranslate.PluginName,
                                             MessageBoxButtons.YesNoCancel,
                                             MessageBoxIcon.Question,
                                             MessageBoxDefaultButton.Button2);
                    }
                    if (dr == DialogResult.Cancel)
                    {
                        cbUseDBForSeeds.CheckedChanged -= cbUseDBForSeeds_CheckedChanged;
                        cbUseDBForSeeds.Checked         = true;
                        cbUseDBForSeeds.CheckedChanged += cbUseDBForSeeds_CheckedChanged;
                        return;
                    }
                    if (dr == DialogResult.Yes)
                    {
                        h.OTPDB_Remove();
                        OTPDAO.RemoveHandler(db.IOConnectionInfo.Path, true);
                        OTPDAO.InitEntries(db);
                    }
                    else if (dr == DialogResult.No)
                    {
                        h.OTPDB_Close();
                        OTPDAO.RemoveHandler(db.IOConnectionInfo.Path, true);
                        OTPDAO.InitEntries(db);
                    }
                }
            }
            m_dDB[db].UseOTPDB   = cbUseDBForSeeds.Checked;
            cbPreloadOTP.Enabled = cbUseDBForSeeds.Checked;
            DBAction_Init(db);
        }
Beispiel #6
0
        private static int ProcessReferences(PwDatabase db)
        {
            //Get DB to work on
            PwDatabase otpdb = db;

            OTPDAO.OTPHandler_DB h = GetOTPHandler(db);
            if (h != null)
            {
                if (!h.EnsureOTPUsagePossible(null))
                {
                    return(-1);
                }
                otpdb = h.OTPDB;
            }
            if (otpdb == null || !otpdb.IsOpen)
            {
                return(-1);
            }
            int i = 0;
            var b = new OTPHandler_Base();

            foreach (PwEntry pe in otpdb.RootGroup.GetEntries(true))
            {
                KPOTP otp = OTPDAO.GetOTP(pe);
                if (!otp.Valid)
                {
                    continue;
                }
                if (!otp.Issuer.ToLowerInvariant().Contains("{ref:") && !otp.Label.ToLowerInvariant().EndsWith("{ref"))
                {
                    continue;
                }
                PwEntry peMain = h is OTPHandler_DB ? (h as OTPHandler_DB).GetMainPwEntry(pe) : pe;
                b.InitIssuerLabel(otp, peMain);
                pe.CreateBackup(otpdb);
                pe.Strings.Set(Config.OTPFIELD, otp.OTPAuthString);
                i++;
            }
            return(i);
        }
Beispiel #7
0
        private static int CheckOTPDataMigration(PwDatabase db)
        {
            const string SEED     = "KeePassOTP.Seed";
            const string SETTINGS = "KeePassOTP.Settings";

            //Get DB to work on
            PwDatabase otpdb = db;

            OTPDAO.OTPHandler_DB h = GetOTPHandler(db);
            if (h != null)
            {
                if (!h.EnsureOTPUsagePossible(null))
                {
                    return(-1);
                }
                otpdb = h.OTPDB;
            }

            List <PwEntry> lEntries = otpdb.RootGroup.GetEntries(true).Where(x => x.Strings.Exists(SEED) && x.Strings.Exists(SETTINGS)).ToList();

            int migrated = 0;

            foreach (PwEntry pe in lEntries)
            {
                ProtectedString seed     = pe.Strings.Get(SEED);
                string          settings = pe.Strings.ReadSafe(SETTINGS);

                string title = pe.Strings.ReadSafe(PwDefs.TitleField);
                string user  = pe.Strings.ReadSafe(PwDefs.UserNameField);
                if (string.IsNullOrEmpty(title))
                {
                    title = PluginTranslation.PluginTranslate.PluginName;
                }
                if (!string.IsNullOrEmpty(user))
                {
                    user = "******" + user;
                }

                KPOTP otp = ConvertOTPSettings(settings);
                otp.OTPSeed = seed;

                otp.Issuer = title;
                otp.Label  = user;
                ProtectedString result = otp.OTPAuthString;

                pe.CreateBackup(db);
                pe.Strings.Remove(SEED);
                pe.Strings.Remove(SETTINGS);
                pe.Strings.Set(Config.OTPFIELD, result);
                if (h == null)
                {
                    pe.Touch(true);
                }
                migrated++;
            }
            if (migrated > 0)
            {
                db.Modified = true;
                if (h == null)
                {
                    Program.MainForm.UpdateUI(false, null, false, null, false, null, db == Program.MainForm.ActiveDatabase);
                }
            }
            return(migrated);
        }
Beispiel #8
0
        private static int OTPAuthFormatCorrection(PwDatabase db)
        {
            //Get DB to work on
            PwDatabase otpdb = db;

            OTPDAO.OTPHandler_DB h = GetOTPHandler(db);
            if (h != null)
            {
                if (!h.EnsureOTPUsagePossible(null))
                {
                    return(-1);
                }
                otpdb = h.OTPDB;
            }
            int i = 0;

            foreach (PwEntry pe in otpdb.RootGroup.GetEntries(true).Where(x => x.Strings.Exists(Config.OTPFIELD)))
            {
                //Don't compare strings because strings are not protected and will remain in memory
                char[] ps = pe.Strings.Get(Config.OTPFIELD).ReadChars();
                try
                {
                    if (ps.Length < 15)
                    {
                        continue;
                    }
                    bool bConvert = false;
                    foreach (char[] check in lOTPAuthStart)
                    {
                        if (check.Length > ps.Length)
                        {
                            continue;
                        }
                        bConvert = true;
                        for (int j = 0; j < check.Length; j++)
                        {
                            if (Char.ToLowerInvariant(check[j]) != Char.ToLowerInvariant(ps[j]))
                            {
                                bConvert = false;
                                break;
                            }
                        }
                        if (bConvert)
                        {
                            break;
                        }
                    }
                    if (!bConvert)
                    {
                        break;
                    }
                    KPOTP otp = OTPDAO.GetOTP(pe);
                    if (!otp.Valid)
                    {
                        continue;
                    }
                    i++;
                    pe.CreateBackup(otpdb);
                    pe.Strings.Set(Config.OTPFIELD, otp.OTPAuthString);
                }
                finally { MemUtil.ZeroArray(ps); }
            }
            return(i);
        }
Beispiel #9
0
        /// <summary>
        /// Perform all kind of migrations between different KeePassOTP versions
        /// </summary>
        /// <param name="db"></param>
        /// <returns>true if something was migrated, false if nothing was done</returns>
        private static bool CheckAndMigrate(PwDatabase db, OTP_Migrations omFlags)
        {
            const string Migration_EntryDB = "KeePassOTP.MigrationStatus";
            const string Migration_OTPDB   = "KeePassOTPDB.MigrationStatus";
            string       sMigrationStatus  = string.Empty;
            bool         bMigrated         = false;


            //Get DB to work on
            OTPDAO.OTPHandler_DB h = GetOTPHandler(db);
            if (h != null)
            {
                sMigrationStatus = Migration_OTPDB;
            }
            else
            {
                sMigrationStatus = Migration_EntryDB;
            }

            OTP_Migrations omStatusOld = OTP_Migrations.None;

            if (!OTP_Migrations.TryParse(db.CustomData.Get(sMigrationStatus), out omStatusOld))
            {
                omStatusOld = OTP_Migrations.None;
            }
            OTP_Migrations omStatusNew = omStatusOld;

            if (MigrationRequired(OTP_Migrations.Entry2CustomData, omFlags, omStatusOld))
            {
                if (!db.UseDBForOTPSeeds() || !db.CustomData.Exists(OTPDAO.OTPHandler_DB.DBNAME))
                {
                    PwEntry pe = OTPHandler_DB.GetOTPDBEntry(db);
                    if (pe != null)
                    {
                        bMigrated = true;
                        OTPDAO.MigrateToCustomdata(db, pe);
                    }
                }
                omStatusNew |= OTP_Migrations.Entry2CustomData;
            }

            if (MigrationRequired(OTP_Migrations.KeePassOTP2OtpAuth, omFlags, omStatusOld))
            {
                int r = CheckOTPDataMigration(db);
                bMigrated |= r > 0;
                if (r >= 0)
                {
                    omStatusNew |= OTP_Migrations.KeePassOTP2OtpAuth;
                }
            }

            if (MigrationRequired(OTP_Migrations.SanitizeSeed, omFlags, omStatusOld))
            {
                int r = SanitizeSeeds(db);
                bMigrated |= r > 0;
                if (r >= 0)
                {
                    omStatusNew |= OTP_Migrations.SanitizeSeed;
                }
            }

            if (MigrationRequired(OTP_Migrations.OTPAuthFormatCorrection, omFlags, omStatusOld))
            {
                int r = OTPAuthFormatCorrection(db);
                bMigrated |= r > 0;
                if (r >= 0)
                {
                    omStatusNew |= OTP_Migrations.OTPAuthFormatCorrection;
                }
            }

            if ((omStatusNew != omStatusOld) || bMigrated)
            {
                db.CustomData.Set(sMigrationStatus, omStatusNew.ToString());
                db.SettingsChanged = DateTime.UtcNow;
                db.Modified        = true;
                Program.MainForm.UpdateUI(false, null, false, null, false, null, Program.MainForm.ActiveDatabase == db);
            }
            return(bMigrated);
        }
Beispiel #10
0
        public override void MigrateFromKeePassOTP(bool bRemove, out int EntriesOverall, out int EntriesMigrated)
        {
            EntriesOverall = EntriesMigrated = -1;
            if (!m_bInitialized)
            {
                return;
            }
            EntriesOverall = EntriesMigrated = 0;

            OTPDAO.OTPHandler_DB h = OTPDAO.GetOTPHandler(m_db);
            if ((h != null) && !h.EnsureOTPUsagePossible(null))
            {
                return;
            }

            PwObjectList <PwEntry> lEntries = m_db.RootGroup.GetEntries(true);

            if (lEntries.Count() == 0)
            {
                return;
            }

            OTPDAO.OTPHandler_Base handler = OTPDAO.GetOTPHandler(lEntries.GetAt(0));
            InitLogger("KeePassOTP -> KeeTrayTOTP", lEntries.Count());
            try
            {
                foreach (PwEntry pe in lEntries)
                {
                    IncreaseLogger();
                    KPOTP otp = OTPDAO.GetOTP(pe);
                    if (!otp.Valid)
                    {
                        continue;
                    }
                    EntriesOverall++;
                    if (otp.Encoding != KPOTPEncoding.BASE32)
                    {
                        PluginDebug.AddError("Migration of entry failed",
                                             "Uuid: " + pe.Uuid.ToHexString(),
                                             "Encoding not supported: " + otp.Encoding.ToString());
                        continue;
                    }
                    if (otp.Hash != KPOTPHash.SHA1)
                    {
                        PluginDebug.AddError("Migration of entry failed",
                                             "Uuid: " + pe.Uuid.ToHexString(),
                                             "Hash not supported: " + otp.Hash.ToString());
                        continue;
                    }
                    if (otp.Type != KPOTPType.TOTP)
                    {
                        PluginDebug.AddError("Migration of entry failed",
                                             "Uuid: " + pe.Uuid.ToHexString(),
                                             "Type not supported: " + otp.Type.ToString());
                        continue;
                    }
                    string settings = otp.TOTPTimestep.ToString() + ";" + otp.Length.ToString();
                    if (otp.TimeCorrectionUrlOwn)
                    {
                        settings += ";" + pe.Strings.ReadSafe(PwDefs.UrlField);
                    }
                    else if (!string.IsNullOrEmpty(otp.TimeCorrectionUrl))
                    {
                        settings += ";" + otp.TimeCorrectionUrl;
                    }
                    pe.Strings.Set("TOTP Seed", otp.OTPSeed);
                    pe.Strings.Set("TOTP Settings", new ProtectedString(false, settings));
                    EntriesMigrated++;
                    if (bRemove)
                    {
                        otp.OTPSeed = ProtectedString.EmptyEx;
                        try
                        {
                            handler.IgnoreBuffer = true;
                            OTPDAO.SaveOTP(otp, pe);
                        }
                        finally { handler.IgnoreBuffer = false; }
                    }
                }
            }
            finally { EndLogger(); }
            MigratePlaceholder(Config.Placeholder, OtherPluginPlaceholder, false);
        }
Beispiel #11
0
        public override void MigrateFromKeePassOTP(bool bRemove, out int EntriesOverall, out int EntriesMigrated)
        {
            EntriesOverall = EntriesMigrated = -1;
            if (!m_bInitialized)
            {
                return;
            }
            EntriesOverall = EntriesMigrated = 0;

            OTPDAO.OTPHandler_DB h = OTPDAO.GetOTPHandler(m_db);
            if ((h != null) && !h.EnsureOTPUsagePossible(null))
            {
                return;
            }

            PwObjectList <PwEntry> lEntries = m_db.RootGroup.GetEntries(true);

            if (lEntries.Count() == 0)
            {
                return;
            }

            OTPDAO.OTPHandler_Base handler = OTPDAO.GetOTPHandler(lEntries.GetAt(0));
            InitLogger("KeePassOTP -> KeeOTP", lEntries.Count());
            try
            {
                foreach (PwEntry pe in lEntries)
                {
                    IncreaseLogger();
                    KPOTP otp = OTPDAO.GetOTP(pe);
                    if (!otp.Valid)
                    {
                        continue;
                    }
                    EntriesOverall++;
                    if (otp.Encoding != KPOTPEncoding.BASE32)
                    {
                        PluginDebug.AddError("Migration of entry failed",
                                             "Uuid: " + pe.Uuid.ToHexString(),
                                             "Encoding not supported: " + otp.Encoding.ToString());
                        continue;
                    }
                    if (otp.Hash != KPOTPHash.SHA1)
                    {
                        PluginDebug.AddError("Migration of entry failed",
                                             "Uuid: " + pe.Uuid.ToHexString(),
                                             "Hash not supported: " + otp.Hash.ToString());
                        continue;
                    }
                    if (otp.Type != KPOTPType.TOTP)
                    {
                        PluginDebug.AddError("Migration of entry failed",
                                             "Uuid: " + pe.Uuid.ToHexString(),
                                             "Type not supported: " + otp.Type.ToString());
                        continue;
                    }

                    string s = "key=" + otp.OTPSeed.ReadString();
                    if (otp.Length != 6)
                    {
                        s += "&size=" + otp.Length.ToString();
                    }
                    if (otp.Type == KPOTPType.HOTP)
                    {
                        s += "&type=hotp";
                        if (otp.HOTPCounter > 0)
                        {
                            s += "&counter=" + otp.HOTPCounter.ToString();
                        }
                    }
                    if ((otp.Type == KPOTPType.TOTP) && (otp.TOTPTimestep != 30))
                    {
                        s += "&step=" + otp.TOTPTimestep.ToString();
                    }
                    pe.Strings.Set("otp", new ProtectedString(true, s));
                    if (pe.Strings.Exists("otp"))
                    {
                        EntriesMigrated++;
                    }
                    if (bRemove)
                    {
                        otp.OTPSeed = ProtectedString.EmptyEx;
                        try
                        {
                            handler.IgnoreBuffer = true;
                            OTPDAO.SaveOTP(otp, pe);
                        }
                        finally { handler.IgnoreBuffer = false; }
                    }
                }
            }
            finally
            {
                EndLogger();
            }
            MigratePlaceholder(Config.Placeholder, OtherPluginPlaceholder, false);
        }
Beispiel #12
0
        public override void MigrateFromKeePassOTP(bool bRemove, out int EntriesOverall, out int EntriesMigrated)
        {
            EntriesOverall = EntriesMigrated = -1;
            if (!m_bInitialized)
            {
                return;
            }
            EntriesOverall = EntriesMigrated = 0;

            OTPDAO.OTPHandler_DB h = OTPDAO.GetOTPHandler(m_db);
            if ((h != null) && !h.EnsureOTPUsagePossible(null))
            {
                return;
            }

            PwObjectList <PwEntry> lEntries = m_db.RootGroup.GetEntries(true);

            if (lEntries.Count() == 0)
            {
                return;
            }

            OTPDAO.OTPHandler_Base handler = OTPDAO.GetOTPHandler(lEntries.GetAt(0));
            InitLogger("KeePassOTP -> KeeTrayTOTP", lEntries.Count());
            try
            {
                foreach (PwEntry pe in lEntries)
                {
                    IncreaseLogger();
                    KPOTP otp = OTPDAO.GetOTP(pe);
                    if (!otp.Valid)
                    {
                        continue;
                    }
                    EntriesOverall++;
                    if (otp.Type != KPOTPType.HOTP && otp.Type != KPOTPType.TOTP)
                    {
                        PluginDebug.AddError("Migration of entry failed",
                                             "Uuid: " + pe.Uuid.ToHexString(),
                                             "Type not supported: " + otp.Type.ToString());
                        continue;
                    }

                    if (otp.Type == KPOTPType.TOTP)
                    {
                        if (Tools.KeePassVersion < m_vKeePass247)
                        {
                            PluginDebug.AddError("Migration of entry failed",
                                                 "Uuid: " + pe.Uuid.ToHexString(),
                                                 "Type not supported: " + otp.Type.ToString(),
                                                 "Minimum required KeePass version: " + m_vKeePass247.ToString());
                        }
                        foreach (var line in m_dTotpStrings)
                        {
                            if (line.Key == otp.Encoding)
                            {
                                pe.Strings.Set(line.Value, otp.OTPSeed);
                            }
                            else
                            {
                                pe.Strings.Remove(line.Value);
                            }
                        }

                        if (otp.TOTPTimestep == 30)
                        {
                            pe.Strings.Remove(TOTPPERIOD);
                        }
                        else
                        {
                            pe.Strings.Set(TOTPPERIOD, new ProtectedString(false, otp.TOTPTimestep.ToString()));
                        }

                        if (otp.Length == 6)
                        {
                            pe.Strings.Remove(TOTPLENGTH);
                        }
                        else
                        {
                            pe.Strings.Set(TOTPLENGTH, new ProtectedString(false, otp.Length.ToString()));
                        }

                        if (otp.Hash == KPOTPHash.SHA1)
                        {
                            pe.Strings.Remove(TOTPHASH);
                        }
                        else if (otp.Hash == KPOTPHash.SHA256)
                        {
                            pe.Strings.Set(TOTPHASH, new ProtectedString(false, "HMAC-SHA-256"));
                        }
                        else if (otp.Hash == KPOTPHash.SHA512)
                        {
                            pe.Strings.Set(TOTPHASH, new ProtectedString(false, "HMAC-SHA-512"));
                        }

                        bool bDummy;
                        MigratePlaceholder(Config.Placeholder, PLACEHOLDER_TOTP, pe, out bDummy);
                    }
                    else if (otp.Type == KPOTPType.HOTP)
                    {
                        if (otp.Length != 6)
                        {
                            PluginDebug.AddError("Migration of entry failed",
                                                 "Uuid: " + pe.Uuid.ToHexString(),
                                                 "Length not supported: " + otp.Length.ToString());
                            continue;
                        }
                        if (otp.Hash != KPOTPHash.SHA1)
                        {
                            PluginDebug.AddError("Migration of entry failed",
                                                 "Uuid: " + pe.Uuid.ToHexString(),
                                                 "Hash not supported: " + otp.Hash.ToString());
                            continue;
                        }

                        foreach (var line in m_dHotpStrings)
                        {
                            if (line.Key == otp.Encoding)
                            {
                                pe.Strings.Set(line.Value, otp.OTPSeed);
                            }
                            else
                            {
                                pe.Strings.Remove(line.Value);
                            }
                        }
                        pe.Strings.Set(HOTP_COUNTER, new ProtectedString(false, otp.HOTPCounter.ToString()));

                        bool bDummy;
                        MigratePlaceholder(Config.Placeholder, PLACEHOLDER_HOTP, pe, out bDummy);
                    }

                    EntriesMigrated++;
                    if (bRemove)
                    {
                        otp.OTPSeed = ProtectedString.EmptyEx;
                        try
                        {
                            handler.IgnoreBuffer = true;
                            OTPDAO.SaveOTP(otp, pe);
                        }
                        finally { handler.IgnoreBuffer = false; }
                    }
                }
            }
            finally { EndLogger(); }
            MigratePlaceholder(Config.Placeholder, PLACEHOLDER_TOTP);             //In case something is defined on group level (could be right, could be wrong, ...)
        }